简体   繁体   中英

"Extensions must not contain stored properties" preventing me from refactoring code

I have a 13 lines func that is repeated in my app in every ViewController, which sums to a total of 690 lines of code across the entire project!

/// Adds Menu Button
func addMenuButton() {
    let menuButton = UIButton(frame: CGRect(x: 0, y: 0, width: 30, height: 30))
    let menuImage = UIImage(named: "MenuWhite")
    menuButton.setImage(menuImage, for: .normal)

    menuButton.addTarget(self, action: #selector(menuTappedAction), for: .touchDown)
    self.navigationItem.rightBarButtonItem = UIBarButtonItem(customView: menuButton)
}
/// Launches the MenuViewController
@objc func menuTappedAction() {
    coordinator?.openMenu()
}

for menuTappedAction function to work, I have to declare a weak var like this:

extension UIViewController {

weak var coordinator: MainCoordinator?

But by doing this I get error Extensions must not contain stored properties What I tried so far:

1) Removing the weak keyword will cause conflicts in all my app. 2) Declaring this way:

weak var coordinator: MainCoordinator?
extension UIViewController {

Will silence the error but the coordinator will not perform any action. Any suggestion how to solve this problem?

You can move your addMenuButton() function to a protocol with a protocol extension. For example:

@objc protocol Coordinated: class {
    var coordinator: MainCoordinator? { get set }
    @objc func menuTappedAction()
}

extension Coordinated where Self: UIViewController {
    func addMenuButton() {
        let menuButton = UIButton(frame: CGRect(x: 0, y: 0, width: 30, height: 30))
        let menuImage = UIImage(named: "MenuWhite")
        menuButton.setImage(menuImage, for: .normal)

        menuButton.addTarget(self, action: #selector(menuTappedAction), for: .touchDown)
        self.navigationItem.rightBarButtonItem = UIBarButtonItem(customView: menuButton)
    }
}

Unfortunately, you can't add @objc methods to class extensions (see: this stackoverflow question ), so you'll still have to setup your view controllers like this:

class SomeViewController: UIViewController, Coordinated {
    weak var coordinator: MainCoordinator?
    /// Launches the MenuViewController
    @objc func menuTappedAction() {
        coordinator?.openMenu()
    }
}

It'll save you some code, and it will allow you to refactor the bigger function addMenuButton() . Hope this helps!

For it to work in an extension you have to make it computed property like so : -

extension ViewController {

   // Make it computed property
    weak var coordinator: MainCoordinator? {
        return MainCoordinator()
    }

}

You could use objc associated objects.

extension UIViewController {
    private struct Keys {
        static var coordinator = "coordinator_key"
    }

    private class Weak<V: AnyObject> {
        weak var value: V?

        init?(_ value: V?) {
            guard value != nil else { return nil }
            self.value = value
        }
    }

    var coordinator: Coordinator? {
        get { (objc_getAssociatedObject(self, &Keys.coordinator) as? Weak<Coordinator>)?.value }
        set { objc_setAssociatedObject(self, &Keys.coordinator, Weak(newValue), .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
    }
}

This happens because an extension is not a class, so it can't contain stored properties. Even if they are weak properties.

With that in mind, you have two main options:

  1. The swift way: Protocol + Protocol Extension
  2. The nasty objc way: associated objects

Option 1: use protocol and a protocol extension:

1.1. Declare your protocol

protocol CoordinatorProtocol: class {
    var coordinator: MainCoordinator? { get set }
    func menuTappedAction()
}

1.2. Create a protocol extension so you can pre-implement the addMenuButton() method

extension CoordinatorProtocol where Self: UIViewController {
    func menuTappedAction() {
        // Do your stuff here
    }
}

1.3. Declare the weak var coordinator: MainCoordinator? in the classes that will be adopting this protocol. Unfortunately, you can't skip this

class SomeViewController: UIViewController, CoordinatorProtocol {
    weak var coordinator: MainCoordinator?
}

Option 2: use objc associated objects (NOT RECOMMENDED)

extension UIViewController {
    private struct Keys {
        static var coordinator = "coordinator_key"
    }

    public var coordinator: Coordinator? {
        get { objc_getAssociatedObject(self, &Keys.coordinator) as? Coordinator }
        set { objc_setAssociatedObject(self, &Keys.coordinator, newValue, .OBJC_ASSOCIATION_ASSIGN) }
    }
}

You can do it through subclassing

class CustomVC:UIViewController {

    weak var coordinator: MainCoordinator?

    func addMenuButton() {
        let menuButton = UIButton(frame: CGRect(x: 0, y: 0, width: 30, height: 30))
        let menuImage = UIImage(named: "MenuWhite")
        menuButton.setImage(menuImage, for: .normal)

        menuButton.addTarget(self, action: #selector(menuTappedAction), for: .touchDown)
        self.navigationItem.rightBarButtonItem = UIBarButtonItem(customView: menuButton)
    }
    /// Launches the MenuViewController
    @objc func menuTappedAction() {
        coordinator?.openMenu()
    }

}

class MainCoordinator {

    func openMenu() {

    }
}


class ViewController: CustomVC {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }

}

Use a NSMapTable to create a state container for your extension, but make sure that you specify to use weak references for keys.

Create a class in which you want to store the state. Let's call it ExtensionState and then create a map as a private field in extension file.

private var extensionStateMap: NSMapTable<TypeBeingExtended, ExtensionState> = NSMapTable.weakToStrongObjects()

Then your extension can be something like this.

extension TypeBeingExtended {
    private func getExtensionState() -> ExtensionState {
        var state = extensionStateMap.object(forKey: self)

        if state == nil {
            state = ExtensionState()
            extensionStateMap.setObject(state, forKey: self)
        }

        return state
    }

    func toggleFlag() {
        var state = getExtensionState()
        state.flag = !state.flag
    }
}

This works in iOS and macOS development, but not on server side Swift as there is no NSMapTable there.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM