Let me add my couple of cents about iOS Design Patterns. Recently I researched (and personally experienced) and was surprised that one of the most popular architecture patterns for iOS applications is MVVM + Coordinators, which means almost 30% of medium and large applications are using this architecture more-less. Yes, MVC is one of the most usable too, but I’m talking about applications like financial instruments, mobile banks, educational apps, shops, delivery apps, and more. It’s understandable why, supporting more complicated, let’s say VIPER, is more expensive because businesses want to see a new feature in production asap. After trying to keep code cleaner, engineers come to the MVVM just because this pattern is more convenient and flexible for scale.
As a software engineer I have the energy to contribute to the community and who knows maybe this particular example will be useful for the iOS community.
Here in this text, I will try to reduce abstractions because from my experience I see sometimes it doesn’t work well to explain complicated things. I will just show you a simplified version. If you are reading this text, you are most likely already experienced guys, you know the basics and are ready to digest and discuss information about at least clean coding approaches. That’s what I want from you after reading this article.
To be more precise about what we are going to do, we need to consider a situation: let’s say we have a family of iOS applications (2 or more applications).
- App One
- App Two
- ...
All these applications are different but with the same UI components, screens, behaviors, and features. And our task will be to design an architecture where all these pieces will be independent and sharable.
After identifying the components which are sharable we can move them into the framework. Let’s call it Common.
So, this framework is our focus and there we will prepare a foundation.
Let’s structure the Common framework where we will create four groups: Views, Base Controllers, Protocols, and Services.
Common
├── Views
├── BaseControllers
├── Protocols
└── Services
In Views, the folder contains only ready-to-use
UIViews
that could be used everywhere in the apps. In our case, we put there ProgressView and BaseButton.BaseControllers
folder contains View Controllers for inheritance. For example, BaseViewController
, and BaseHostingController
for SiwftUI views, and BaseNavigationController
. All these base controllers already have a property of ProgressView
which can be started and stopped spinning.The protocols folder consists of all protocols which are common, for example,
BaseViewModel
, Coordinator
, and so on. We will figure out about them later.Services. Let’s say our family applications use some services and part of them are sharable, for example,
CommonServiceOne
and CommonServiceTwo
.Designing dependencies in our case means we will combine all our services into one place and this place will be reachable and can be configurable from all view models. The main idea is that our Common Dependencies object should be opened for inheritance from the applications which are also can have some services which will lay together with common services.
As you already know we have two common services. Let’s create two protocols for them:
HasCommonServiceOne
and HasCommonServiceTwo
.As you can see protocol Dependencies are inherited from the two protocols that require to have the services. The property appId will allow identifying an app which are using the services. It’s helpful for some requests that should contain app id in the paths.
Finally, we have a class that conformed Dependencies protocol with initialized our two services.
Regarding the services, they have just two methods that print the line with the app id:
CommonServiceOne
func commonServiceOneMethod() {
print("== Common Service 1 method fired from \(appId)")
}
CommonServiceTwo
func commonServiceOneMethod() {
print("== Common Service 2 method fired from \(appId)")
}
So we will use it later to understand from where we can access them.
Dependencies should be accessible from view controllers so for that we need an additional protocol that can be named
WithDelegates
:The protocol Coordinator has typical requirements for any coordinators in the projects. It means it can be also in a common framework. Let’s take a look at what is in there:
If you are familiar with the Coordinator design pattern then you should know already why we need these methods. Super easy,
start()
— for starting the coordinator with an initialized view controller, and finish()
— for poping view controller from the navigation stack. As you can see this protocol has an extension where we can predefine this behavior for both methods.We need an additional protocol to combine two View Controllers:
BaseViewController
and BaseHostingController
. As you know they have ProgressView
, and two methods that can start and stop spinning. We are going to move these methods and define requirements for the Base Controllers.Of course, if the apps
haveViewModels
then we need to identify common methods. Let’s say it will be: prepareViewModel()
. Additionally, to interact and delegate some jobs to the ViewControllers
we need to have a viewDelegate
.Two delegates. The first one is for View Controllers and the second one is for Coordinators. Let’s take a look at them.
BaseViewModelViewDelegate
Using this protocol for all
viewDelegates
we will allow to show and hide activity indicators without repeating these methods in app’s viewModelViewDelegates
.BaseCoordinatorDelegte
A similar approach in
coordinatorDelegate
.BaseController
protocol combines two ViewControllers
where viewModel
is BaseViewModel
.All basic components of our framework are done, so it’s time to use them in our applications. It's time to define an app structure:
Based on the schema above, we need to prepare:
AppCoordiantor
, AppDependencies, Home Module, SignIn Module. Using the prepared Common Framework we can use the protocols from there to prepare all these pieces of the app.Let’s say an application has its own services and we need to combine them with shared services. For that our protocol Dependencies and
CommonDependecies
opened class will help us.As you can see additional app services are initialized in the same way as in the common dependencies. So, for now, our
AppDependencies
has 4 services that we can use across the app.In this implementation, you can see how we can use
WithDependencies
protocol. AppDependencies
will be used in all modules: Home
and SignIn
. Also, take a look at HomeViewModel
coordinator delegate and how it is used. To push the view controller to the stack it’s enough to use only show()
method that already exists in Coordinator
protocol.The most interesting part is using view models. Again, our view models don’t know about the view and coordinator, that’s why they are independent. Common protocols:
BaseViewModel
and WithDependencies
help to encapsulate an implementation where we can potentially use dependencies.Also in
prepareViewModel()
we can use directly some commonServieMethod()
that is defined already in BaseViewModel
. But, wait, BaseViewModel doesn’t use protocol WithDependencies
. So, let’s say we need to use it in all our applications from all viewModel
. For that we need to update BaseViewModel
in Framework using this way:This means the
methodsomeCommonServiceMethod()
can be used only from View models but not from the views.Additionally, if you don’t need all dependencies in the view model you can specify the particular ones:
final class SignInViewModelImplementation<Dependeincies: HasAppServiceOne & HasAppServiceTwo>: SecondaryViewModel, WithDependencies {
}
An idea is simple - using the protocols and the classes with associated types from the Common framework your next app with MVVM + Coordinator will be structured in the same way as the first one but your modules and behavior can be different.
The services, coordinator, and base controller will take common responsibility. In this example, it's only showing and hiding activity indicators, but in real applications, you could have a lot of staff. For instance, shared localization resources and a service for managing the strings by a key that can be used from all applications in family.
Recently I had a problem when I needed to prepare a framework with sharable components for multiple applications. Making this kind of framework allowed me to move maintainability to the next level. It means we could scale our projects and this framework without discussing it. Everyone from the team understood where and what they need first before starting a new feature.
As you noticed the barrier of entry is really low because you don’t need to know specific things, everything implements in a swifty way. Also, it can be applied with RxSwift or Combine as well.
Thanks for reading!