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?

Jessica Kimble
  • 493
  • 2
  • 7
  • 17
  • If what you need is a single coordinator instance you can create a singleton shared instance. https://stackoverflow.com/a/47481780/2303865 – Leo Dabus Sep 15 '19 at 17:09
  • How is this coordinator created? is it the same for everyone or is it unique to a group of controllers? – Alex Sep 29 '19 at 08:12

6 Answers6

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!

Zoe
  • 96
  • 5
5

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

}

Mussa Charles
  • 4,014
  • 2
  • 29
  • 24
  • This will allow it to compile, but it doesn't allow the subclasses of `ViewController` to actually set the `coordinator`. – vacawama Sep 15 '19 at 14:36
  • @vacawama 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. – Mussa Charles Sep 15 '19 at 14:47
  • 1
    If that is all the OP needed, they could have just replaced `coordinator?.openMenu()` with `MainCoordinator().openMenu()` and skipped the whole variable. – vacawama Sep 15 '19 at 14:49
  • I think calling MainCoordinator().openMenu() will make MainCoordinator a singleton rather than just a separate instance for the specific ViewController and cause unintended results. So making a separate property on VC make it stand as a separate instance special for the VC which uses it. – Mussa Charles Sep 15 '19 at 15:23
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) }
    }
}
Casper Zandbergen
  • 3,419
  • 2
  • 25
  • 49
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: 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) }
    }
}
marcelosalloum
  • 3,481
  • 4
  • 39
  • 63
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.
    }

}
Shehata Gamal
  • 98,760
  • 8
  • 65
  • 87
  • 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. – Jessica Kimble Sep 15 '19 at 13:35
  • Also calling CustomVC().addMenuButton() will not execute any action. – Jessica Kimble Sep 15 '19 at 13:39
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.

Sri Harsha Chilakapati
  • 11,744
  • 6
  • 50
  • 91