Swift Sun Swift Sun - 2 months ago 18
Swift Question

Passing information between controllers in a Swifty (/protocol) way?

I am trying to pass information from controller A to controller B. The thing is, I want:


  • Concise: To minimize the autocomplete from
    XCode
    to some key information. I want to know in a easy way the exact parameters required before pushing the controller on the stack.

  • Avoid segues. From my understanding, segues create a lot of tight coupling in the storyboard. I don't want to be dependent of the storyboard to pass information. (Each time I want to switch controller A to another, I have to go to the storyboard to make some changes). I might split my app in several storyboards at some point, and segues can be quite annoying to deal with.

  • Beautiful: Maybe Swift can provide a Swifty solution I have not thought about.



What I have been trying to accomplish at first was to push controller as protocol. Even if it not possible, let me explain:


  • Pushing a controller as a protocol would allow me to have exact visibility on my attributes.

  • There is no tight coupling related to the storyboard

  • Many controllers (A, C, D) might want to push a B controller, I might give them each one of them a different protocol to push controller B. Maybe B could appear under different circumstances.



At first, my code was looking like that (the storyboard extension works):

if let myBCustomVC = storyboard.instanciateControllerWithIdentifier(MyBCustomVC.self) as? myVCustomFromAProtocol{
myBCustomVC.AToB = self.data.count
self.navigationController?.pushViewController(myBCustomVC, animated: true)
}

protocol myVCustomFromAProtocol {
var AToB: Int {get set}
}


The thing is, I cannot push a view controller downcast to a protocol. I had to come by an ugly
UINavigationController
extension. Here is the full result.

if let myBCustomVC = storyboard.instanciateControllerWithIdentifier(MyBCustomVC.self) as? myVCustomFromAProtocol{
myBCustomVC.AToB = self.data.count
self.navigationController?.pushViewController(vcToPush: myBCustomVC, animated: true)
}

extension UINavigationController{
func pushViewController(vcToPush: Any, animated: Bool){
if let vc = vcToPush as? UIViewController{
self.pushViewController(vc, animated: animated)
}
}
}


Let's face it, while achieving my first two goals, I have downcasted an Any to a
UIViewController
, woosh.

Is there any way to avoid tight coupling and push controllers on the stack in a beautiful way while keeping a limited visibility on the parameters to pass from the first view controller(A) to the second(B). What are your thoughts? Why wouldn't I want to do that?

Answer

With the help of Fogmeister I came up with this answer. It has several advantages and disadvantages over the use of a Presenter Object that inherits from a protocol.

Solution: Use a static factory method (or several depending of the use case) located in the controller to be instantiated.

Advantages:

  • Using a static factory method enforces to pass parameters that the actual controller know to be required for his own initialization.
  • No structure involved (less complexity?)
  • The constant use of a static factory method becomes natural over time. You know when looking at a controller you must implement the method to call it, and you know the parameters required. You do not have to worry which parameters must be implemented to push the controller, just implement the function. Using a presenter, you do not have this knowledge as the unwrapped parameters can be numerous.
  • The use of a Presenter is closer to MVP and gets away from the dependency injection model advocated by Apple (MVC)

Disadvantages:

  • The use of the Presenter pattern suggested by Fogmeister uses a protocol. The protocol gives limited visibility to controller A (and all controllers that want to push a controller B).
  • The use of the Presenter pattern gives more modularity and reduces tight coupling. Controller A has no need to know anything about controller B. The replacement of controller B to another controller C can be done without affecting all the controllers that were going to B (can be quite useful if you are restructuring your application or in some use cases).

Here is my current implementation in my class:

In controller B (static function):

static func prepareController(originController: UIViewController, AToB: Int) -> bVC? {
    if let bVC = originController.storyboard?.instanciateControllerWithIdentifier(self) as? bVC{
        bVC.AToB = AToB
        return bVC
    }
    else{
        return nil
    }
}

In controller A (to push the controller on the stack):

 if let bVC = bVC(originController: self, AToB: *someValue*){
     self.navigationController?.pushViewController(bVC, animated: true)
 }

Solution has to be taken with a grain of salt, let me know what you think @Fogmeister?

Both solutions can be combined. Using a protocoled structure that would use the static method provided by the controller. The thing is it can quickly become overly complex for people you introduce to the project. That's why I am sticking to my solution at the moment. I am looking at frameworks for dependency injection though.