The Model-View-Controller (MVC) is a design pattern Apple choose for the iOS. I think it is a good choice because it easy for newcomers to grasp and flexible enough for adaptation as you projects grow.
Once you projects grow in size and complexity, we try to find a solution and we end up with a new architecture (and a new problem), that’s why we see many new architectures coming out recently.
This post isn’t about a new architecture, I just want to introduce you to a new piece of code with one specific task, control your application flow. I would say it just another C (controller) in MVC.
First, look at following code. Most of you should familiar with it.
This is a generated boilerplate when you create a new Master-Detail app project in Xcode.
Actually there is no obvious problem with this approach. If your app’s flow is simple and each view controllers only used once. You can get away with this approach. It serve me well for many years and still work in most of my projects.
You will realize some direct and indirect problems if your app have complex flow with many screens in one flow.
Before jumping to the problem let see some a sample, so we all get an idea of how complex flow I’m talking about. Actually it doesn’t need to be rocky science app to see this problem.
Here we have shopping app where users can make a purchase or physical products. As time goes by, we want to focus on digital product and ditch out all physical goods.
Here is our new flow, we removed 2 view controllers from our flow.
I won’t provided all classes of this sample project, but you can guess my naming and what does it do.
First, we skip 2 view controllers, so we have to change code in OrderSummaryViewController to new destination.
This might look like a simple change because both DeliveryAddressViewController and ShippingOptionViewController have same interface in this case which is Cart. Cart keep track of all data in the flow.
When some object keep track of “all data in the flow” this is a sign of code smell. Cart object contain too much information. All view controllers in the flow have to access this object which in fact they only need a subset of that information i.e. OrderSummaryViewController want to know only products in a cart, DeliveryAddressViewController want to know only addresses.
This inter-dependencies of view controllers make it hard to do proper dependency-injection, this left us with some ugly choice like global/shared state object like Cart or even singleton to pass along information to the end of the flow.
This global/shared state cause a problem when you work as a team since no body want to touch this sacred Cart knowing that everyone are using it and changing it might affect others.
As you can see there are some problems about this design choice.
Problems of this approach coming from the fact that a view controller know too much about next view controller. They are highly dependent on one another. A class assumes too many responsibilities and one concern is spread over many classes.
This lead to many more problems.
- Reusability problem
When a class have many responsibilities and concerns, it hard to reuse this class elsewhere.
- Hard to do dependency-injection
The one who do injection is the former view controller in the chain, this make a view controller have extra concern.
- Global/Shared state
Everything that needed at the end of the flow need to be pass along or everyone reference global state.
These inter-dependencies are not always known by other developers or even yourself (few months later) and if there is a change in the flow (and you know it will be) it would be difficult to do so.
The fact that we have to use Cart to remember all the things and pass along the flow imply that there is a missing piece of class that should responsible for this.
I think Coordinator is a missing piece of this problem.
There are many Coordinator articles out there, you can search for variety of Implementation and description. I will keep it simple and want you to think as a simple PONSO (Plain Old NSObject)/POSO (Plain Old Swift Object).
The idea of the Coordinator is to create a separate entity which is responsible for the application’s flow. In this case I want this class to responsible for handle purchasing flow, so it will know the presentation flow and data needed in the flow.
Quite a chunk of code here’s some break down.
All view controllers are now focus on one job, interface are clearer as you can see OrderSummaryViewController not accept Cart.Item not Cart. View controllers communicate back to whoever interest by old friend, delegate, nothing fancy.
Cart also more reasonable only keep number of items in a cart. Everything else are move to their own class ShippingOption and PaymentMethod and coordinator holding them.
Coordinator hold all data necessary for the flow and control the flow by present and dismiss view controllers.
You can simply using it like this.
I want to keep the implementation simple here, not using any protocol or anything fancy, but I think you can see some benefit of using a coordinator. Responsibility and concern of each classes are more clear now, each class doing one job and delegate to others as soon as they can.
Coordinator might get massive over time, but nothing preventing you from create sub coordinators (where it make sense). Personally I don’t mind line of code, if the flow is long I can live with large coordinator with many line of code as long as it does one job, coordinate the flow. You can come up with anything fancy here, but just
// MARK: is enough for me to keep code organized.
Feel free to follow me on Twitter and ask your questions related to this post. Thanks for reading and see you next time.
If you enjoy my writing, please check out my Patreon https://www.patreon.com/sarunw and become my supporter. Sharing the article is also greatly appreciated.Become a patron Tweet Share
Enum & custom type from primitive JSON type
MVC (Missing View Controller)
Where is UIViewController in SwiftUI?