iOS App Architecture: MVVM-C at Scale
Code Sample: Here
During last one year there was a big task at hand, how to modularise application with an architecture that have following considerations.
- SOLID should be practised.
- Code should be based on an architecture with less boilerplate code, thus an unofficial industry standard called
MVVM-C
was obvious choice. - Every layer should be interfaced via
dependency management
and lays foundation for high level DI-Framework. - Code should be easily
testable
and mocks can be easily replaced with original objects. - Project should be structured to
scale,
as big as it can be. Folder structure
should be highlighting the architecture.- Shared areas and team based divisions should be implemented as independent
Modules (Swift Package)
- Layers should be interfaced together via
Protocols.
- Architecture should be close to Android so that we can resolve problems at once and use on multiple platforms.
- Code should be following
iOS Swift Style Guidelines
and have similar styling throughout the code. - Code should be easily extendable, maintainable and understandable.
After discussions at length we came to a conclusion to bench mark the following architecture.
Unmarked arrows show ownership.
Its a MVVM-C architecture with few more layers to cater special needs. Layers are defined here.
Coordinator
Application Screen-flow and Push Routing handling should be done in this layer.
Model
Model acts as an entity. In sample code model is shared entity between Codable and NSManagedObject, thus needs some boiler-plate, as both APIs do not go along automatically.
View
Display of data can be done with Storyboard or in code. If Component based design system i-e Style Dictionary is under consideration in future, it’s better to go with code based user interfaces i-e UIKit in code or SwiftUI.
Data is being displayed in the form of Models (viewData/ViewModel). Views conform to Model Configurable to populate data via configure(model:)
method.
ViewController
Controller contains logic of view’s state representation and pass actions to viewModel and listens for actions from viewModel.
ViewModel
- ViewModel is only stateful layer and does contains all the business logic, with or without the help of mappers.
- Also it informs view-controller about the state update via
bindings.
- Bindings can be implemented by any method of your choice closures, delegates, observers, Combine, RxSwift and Promises etc.
Mapper/UseCases (Optional)
Mapper are the Domain Usecases
. Any domain logic will be injected in the form of independently tested usecases. Idea is to keep Domain-Layer
independent from ViewModels thus can be widely reuseable
. Also on higher level there can be an independent module of just mappers/usecases, written in a cross-platform technology i-e Kotlin Native and exported for both iOS and Android. This can set a foundation for shared domain logic between both platforms.
Repository
Its a facade and abstraction layer
between ViewModel
and Data Stores
that encapsulates Data-source layer. Currently there are two data sources, Network and CoreData in example. This layer can also encapsulate further data sources i-e Firebase Remote Configurations etc.
Interactor
This layer interacts with network interface that fetches and decodes data. It’s a wrapper written on top of Swift Package Ceres
which fetches and decodes data into inferred model object.
Persister
Persister encapsulates CoreData and fetches and updates data. [Edit] An (optional) abstraction protocol i-e Persister, can be placed between CoreDataService and Repository to make unit testing simpler.
Other
Other data sources can also be part of your repository, i-e Firebase remote config to turn features on/off or for giving arrangement of features in a particular flow etc.
Sample Code:
This architecture reference is backed with a sample project available here. Please be critical and leave your comments here or create issues on Github.
Conclusion
There are tons of ways to create an application architecture, as there are no rights or wrongs, at the same time there are tradeoffs that can be expensive. Keeping expense to bare minimum and robustness as high as possible, it is an effort to introduce iOS app to modern modular architecture.