As Roy Marmelstein said in his recent presentation at dotSwift - "We love to be excited". Can not agree more. When we read a blog post that describes the concept or architectural pattern that is somewhat new for us and feels exciting we tend to accept it as something good. We can go really far away this rote and sometimes we don't notice that we already have all the tools to solve the problems they address. For iOS developers this tool is a framework that we inevitably use every day - UIKit.
That's what I was thinking about in the breaks at dotSwift. It started to bug me after I watched yet another presentation about coordinators the night before. Not saying that this is a misleading concept (it's really not) let's think how can we do the same using what UIKit had for ages.
All of that can be also applied to VIPER and all of it's derivatives as its Interactor and Router are doing the same job as Coordinator.
The idea behind coordinators is very clear. They are there to manage application flow. You have a root (or app) coordinator that has references to child coordinators for each of the user stories. Then each of the child coordinators completely manages flow in their story and can transition to other stories (via its parent) or start new "child" stories. They let you make your view controllers highly maintainable: you can change their order in the user story, you can change the way how they are presented, you can easily extract them and reuse in completely different environment. In other words you can easier change the flow. In theory the only thing that will change when flow changes, if properly implemented, is coordinator.
That's all good, but its usually suggested to use PONSO objects as coordinators. And here is the obvious alternative that UIKit gives us that is for some reason ignored - subclassing view controllers. I used to also ignore it and advocate for PONSO objects instead because what can be easier than dealing with PONSO, right? But starting to think about it I didn't find any critical arguments against it.
Subclassing standard UIKit controllers
There is probably no application in the AppStore that does not use
UINavigationController. If you find yourself thinking about implementing your own version of it - stop and think again. Then think once more. If you still what to go for it you might have some specific reasons, but most of the time you will be completely covered by using these controllers. Even if you use them as-is you are able to customise their behaviour, i.e. transition animation.
There are always exceptions. Don't even think about using
UIPageViewControllerif you want to keep your crash free rate high without trying to fix every hole in it. Can't say much about
UISplitViewControlleras I never used it, but I suspect it's more likely that you will need to adjust it and then it may not play nice with you. In case of
UITabBarControllerI've never had to change their default behaviour.
UINavigationController documentation says: "You generally use this class as-is but you may also subclass to customize the class behavior."
UITabBarController documentation says: "This class is generally used as-is but may be subclassed in iOS 6 and later." We subclass
UIViewController without even questioning, so there is nothing that stops you from subclassing
UITabBarController and use them as your coordinators. It's exactly what they are designed for. They are not just dummy view controllers. They literally manage their child view controllers flow in their own way. The same if you use you custom container - it can be a coordinator for its child view controllers.
If you need custom actions, need to store and update some global state, pass data between view controllers you can use view controller subclass the same way as you would use PONSO coordinator. With one difference - you will not need another abstraction. Less abstractions - less things to wrap your (and your colleagues) head around.
If you are concerned about testability - there are no show-stopper issues in unit testing
UITabBarController, they are just view controllers, so you unit test them the same way. All that buzzing about "view controllers are not testable" is just an excuse for not testing them (that I myself use too often with no real reason, just because I don't like to write tests that much) because they are usually monolithic. Of course Massive-View-Controller is hard to test. But any God Object is hard to test, it's true not just for view controllers. Thin view controller does not mean dumb view controller.
If you are concerned with tight coupling your view controller with navigation or tab bar controller then just don't couple them too much! There is no much difference here in using PONSO or view controller subclass. Don't rely on your view controller being in navigation stack or being presented modally, as this can change and this is not a view controllers responsibility. The only benefit of using PONSO here is that you can make it's interface not exposing anything related to concrete implementation of navigation, i.e. if it uses UINavigationController internally or something else. With subclass due to inheritance client will be able to access all UINavigationController methods. But the thing is that it's their problem if they use it the way that they tightly couples with such implementation details. They could use protocol that does not leak any implementation details instead of concrete class. With subclass they just have more options (and more ways to screw up), choosing PONSO instead just constrains its client so they can not "abuse" it in some ways, but I would not say that this should be a reason to prefer it. That's simply the question of client using dependency injection correctly or not.
If you are concerned with subclassing because you don't want to inherit unneeded behaviour or you don't want your code to break with the next iOS update then it's just too late - you have to use UIKit and most likely you already use it's containers. Then it does not matter either you use
UINavigationController as-is and PONSO coordinator or you use
UINavigationController subclass itself as coordinator. It's even better this way as you can override unneeded behaviour.
The only thing you need to successfully implement application flow is to remember that your view controllers should be agnostic to it. Screen A should not ask whoever (router, interactor, storyboard, it's navigation controller or tab bar controller) to present screen B, either modally or with a push, screen A should not pass data to screen B, screen A should not care if it's presented modally, pushed in navigation stack or presented in a pop over. If you do that you are coupling your controllers with your current flow. It will make harder to use them separately from each other or change that behaviour. Instead you should delegate all of that to someone else. It may be a PONSO coordinator, but the same way it can be view controller's parent - navigation, tab bar controller or you custom container. You just should not leak their implementation details.
Of course there are cases when it's not necessary. If you have a screen for creating a calendar event and screen to set its time you most likely will always use them together in one way. Then it's not a problem to couple them together. You still can avoid too much coupling using abstractions instead of concrete types. But you don't have to use coordinator of any form in this case.
The powerful technology that you probably almost never use explicitly. But if you use it you will see that you don't have to define all the
IBActions in the view controller or a view that contains controls that trigger those actions and then pass them to a delegate or call some block. You can put them right in there in the controller that should handle this action. If this action requires some flow transition it most likely belongs to coordinator. If you are using view controller based container instead of PONSO coordinator responder chain will be there to help you to wire things together. Just define
IBAction right in your container and link it with First Responder in Interface Builder. If you would use PONSO coordinator you will not be able to do that as it's not easy to insert PONSO in responder chain and you will end up with doing some extra work: you will need to define
IBAction in view controller and use it to call a method on coordinator which will also most likely involve using delegate protocol, or you will need to set target and action in code. That's all just unnecessary boilerplate that no one likes to write.
This is another UIKit feature that, as far as I can tell from my experience, is not widely adopted. I myself used it twice or so and just as an experiment. The same as responder chain (it actually is built on top of responder chain) it let's you to define actions, but for leaving screen, and it let's you to define these actions in the view controller that needs to handle the result of the job performed by user (or your app) on dismissed screen, or in a container that plays the role of coordinator. Unwind segue will also automatically dismiss view controller, so you don't have to pop or dismiss it manually. Another improvement they can bring is that you can get rid of endless delegate methods or completion blocks. You can even use custom segues for unwinding and trigger them manually.
Note: from what I've tried it looks like if you put an action for unwind segue in navigation controller subclass it will become its destination and dismissed view controller will not be popped automatically. The dismissed view controller will still receive prepare for segue callback, so if you delegate this method to its coordinator (navigation controller in this case) you will be able to handle that action there and will not need to dismiss controller manually, UIKit will handle it.
There is a valid concern regarding using responder chain and unwind segues. They are (mostly) managed by UIKit and we don't have (you have some control of unwind segues) a lot of ways to control it. So you of course need to understand how these techniques work, but still you can face some unexpected behaviour when they don't work nice with what you need to do (see my previous note). Using PONSO controller you have more control over it, but it comes with a cost of writing some boilerplate and introducing a new level of abstraction. So I would say this is the only thing that you should have in mind when deciding to use these techniques. The point is that you have options, what is always good.
One more tip
Use code generators like R.swift or SwiftGen to generate code for working with segues, storyboards and view controllers in a type-safe manner. They don't just provide strongly typed APIs instead of UIKit's stringily typed, they will save you from leaving potential bugs and crashes when you change your flows, as they will prevent your code from compiling until you fix all the issues caused by flow changes.
In the end as you can see using coordinators does not necessary mean introducing new abstractions, it can be simply using standard UIKit components in a better way. Nothing said here is a rule set in stone though, except maybe using code generators (that you should really do even if you are not using coordinators). This is more like a set of advices that I'm myself going to try out and will follow up if I find something that does not work out well. If you know something that I might miss - please do share your thoughts in the comments.