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

6

8

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?

Mccready answered 15/9, 2019 at 13:12 Comment(2)
If what you need is a single coordinator instance you can create a singleton shared instance. https://mcmap.net/q/904369/-ble-peripheral-disconnects-when-navigating-to-different-viewcontrollerVerniavernice
How is this coordinator created? is it the same for everyone or is it unique to a group of controllers?Deter
I
7

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!

Iinden answered 30/9, 2019 at 21:57 Comment(0)
H
6

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()
    }

}

Hurter answered 15/9, 2019 at 14:11 Comment(3)
This will allow it to compile, but it doesn't allow the subclasses of ViewController to actually set the coordinator.Terriss
@Terriss yeah that's why it is a computed property it is automatically set from its instance as a get only property when the Viewcontroller is initialized.Hurter
If that is all the OP needed, they could have just replaced coordinator?.openMenu() with MainCoordinator().openMenu() and skipped the whole variable.Terriss
L
4

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) }
    }
}
Lyssa answered 1/10, 2019 at 8:12 Comment(3)
Note that OBJC_ASSOCIATION_ASSIGN doesn't store a weak reference, it stores an unowned one, even if the documentation says otherwise. You could use a box to avoid the problem.Antoniettaantonin
@Antoniettaantonin interesting, I didn't know this, would getting the value after it's been released crash?Lyssa
Yes, it would crash :)Antoniettaantonin
T
2

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: AnyObject {
    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) }
    }
}
Tyratyrannical answered 2/10, 2019 at 21:7 Comment(1)
Note that OBJC_ASSOCIATION_ASSIGN can lead to crashes, if the coordinator deallocates.Antoniettaantonin
A
0

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.
    }

}
Antinomy answered 15/9, 2019 at 13:21 Comment(2)
Thanks for your answer, Inheritance wont work in my case, as I'm already inheriting from a protocol that instantiates my storyboards in every ViewController. So that would be multiple inheritance which isn't allowed.Mccready
Also calling CustomVC().addMenuButton() will not execute any action.Mccready
B
0

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.

Bumblebee answered 29/3, 2021 at 13:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.