VIPER Architecture on iOS in Details
#iOS App Development
There are tons of VIPER-related publications in the internet, and still, every team cooks VIPER in it's own way.
Here is my recipe that I'm using in my current project.
Furthermore I've already promised as one my mates has asked to help him with VIPER recently. So I'm jumping in.
VIPER, WAT?
Basically, every screen is considered to be independent VIPER module, consisting of
- V - ViewControler
- I - Interactor
- P - Presenter
- R - Router
E stands for Entity probably just to make it sound more snaky.
ViewController, View
Thanks to UIKit, these two guys are often so tightly coupled (especially when implemented in Storyboard) that it's usually no sense to separate them. My point is to make them as plain and thin as possible. No state, no extra logic, just "render" the input. Sometimes they are also responsible for animations. That's it.
In my implementation, ViewController performs some appearance and layout setup, and delegates everything else to Presenter. Everything starting from VC lifecycle handlers (viewDidLoad, viewWillAppear, ...) and ending with buttons touch actions.
For example, if VC has a UITableView, it conforms to UITableViewDataSource and UITableViewDelegate protocols, but the are super-thin.
Presenter
VC events. First of all, Presenter handles ViewController events. In most cases it handles events in two ways:
- Asks Router to route to another module
- Asks Interactor to perform something: fetch data, or perform some action that requires service method call.
For example, when handling VC's viewWillAppear event, it will ask Interactor to perform initial refresh of data.
Presentation events. From the other point of view, it handles "present" events, coming from interactor. When data is fetched or changed, Interactor calls Presenter's methods, like:
func presentCollectionUpdate(_ update: CollectionUpdate) {
...
}
enum CollectionUpdate {
case beginUpdates
case endUpdates
case insert(idx: [IndexPath])
case delete(idx: [IndexPath])
case insertSections(idx: [Int])
case deleteSections(idx: [Int])
case updateSections(idx: [Int])
case moveSections(from: Int, to: Int)
case update(idx: [IndexPath])
case move(from: IndexPath, to: IndexPath)
}
Entities -> ViewModels One more responsibility of Presenter is to turn raw Entities into ViewModels.
The point is that Interactor operates with Entities while ViewController works with ViewModels. ViewModels are just DTOs that are already prepared for simply showing (let's call it rendring) in View without any need to change or prepare data.
Common Presenter's API has:
func numberOfSections() -> Int
func numberOfItemsInSection(_ section: Int) -> Int
func itemViewModelAt(_ indexPath: IndexPath) -> UserViewModelProtocol
While Interactor's API looks like:
func numberOfSections() -> Int
func numberOfItemsInSection(_ section: Int) -> Int
func itemAt(_ indexPath: IndexPath) -> UserProtocol
Delegating. There are cases, when you need to present some module and pass some delegate to it. For example, when you need to search and pick something and then use in current module.
I usually make current module's Presenter conform to delegate's protocol and pass it through Router's method call to another module.
Interactor
Interactor is all about data. Usually it performs API calls and save the results to a local storage. It doesn't do it directly, but with the help of Services.
For example, if we have some user service with a protocol:
protocol UserServiceProtocol {
func searchUser(_ username: String,
complete: @escaping ResultCompleteHandler<[UserProtocol], Error>)
}
Interactor will get the list of users and will use StorageService to save obtained objects.
Interactor is likely to work with some local DataSource, like CoreData FRC to keep track of fetched objects and process collection update events.
One of my recent mistakes is that I didn't wrap FetchResultsControllers into some abstraction. So all my Interactors if they work with managed objects collections, they do it directly via FRC. Not very nice, but still way better than having FRC in ViewController in plain old fat MVC.
The good point of my current implementation is that Interactor does not expose its FRC internals. So even if it works with UserManagedObjects, it exposes only UserProtocol in its API.
Router
The most simple. It usually has routing methods like:
func routeToUserProfileFor(_ user: UserProtocol) {
AppModules
.UserProfile
.userProfileContainerFor(user)
.build()?
.router.present(withPushfrom: self)
}
So Presenter just asks Router to go somewhere and Router decides which module to build and asks newly created module's router: "Hey, man, present your module right from here!"
That's rather convenient when you need to flex navigation in your app because you don't need to touch or change other components of the module.
Custom interactive transitions. They turn everything into a pain because they start from gesture handling on the very top in current VC, and in fact you need to pass UIViewControllerInteractiveTransitioning, presentation and dismiss animators to the newly presented VC.
In order to reduce pain, I've wraped it in a more or less convenient way and currently it looks like this:
After handling VC's action, Presenter delegates everything to Router:
func handleInteractiveActionWith(_ transition: GestureInteractionControllerProtocol) {
router.routeToSomeModuleWith(transition)
}
(In the VC the action is tiggered by Gesture Recognizer, that is responsible for custom transition)
Router's implementation of routing method:
func routeToSomeModuleWith(_ transition: GestureInteractionControllerProtocol) {
let module = AppModules
.SomeModuleDomain
.someModule
.build()
module?
.view
.transitionsController
.presentationAnimator = SlideInAnimationController()
module?
.view
.transitionsController
.presentationAnimator?
.interactiveTransitioning = transition
module?
.view
.transitionsController
.dismissalAnimator = SlideOutAnimationController()
module?.router.present(withPushfrom: self)
}
Not diving into details, GestureInteractionControllerProtocol covers object that should handle gestures and animation progress. In fact, it's inheritant of UIKit's UIViewControllerInteractiveTransitioning protocol.
Router also creates animator controllers for presenation and dismissal. They are impelmentations of UIViewControllerAnimatedTransitioning and define how the animation should look like.
Child-Parent. There are cases, when one VIPER module is embedded into another VIPER module and its view conroller will be added as a child from the UIKit point of view.
In that case, Presenter has to ask ViewController for some container view and ask Router to show some module in that view:
Presener implementation:
func handleShowWalletAction() {
router.routeToWalletPreview(in: viewController.walletContainerView)
}
Router implementation:
func routeToWalletPreview(in container: UIView) {
let module = AppModules
.Wallet
.walletPreview
.build()?
.router.present(from: self, insideView: container)
}
Each module also has it's own ModuleAPI, ModuleConfigurator and ModuleScope.
ModuleAPI
Describes protocols, that each of V, I, P, R should conform to. BTW, each of V, I, P, R depend on protocols, instead of exact implementation.
Good thing for two reasons:
- You can can several implementation of each module and choose from them
- Module API works like a doc, describing what's going on in the module.
ModuleScope
Modules are designed to be as independent as possible. For the modele ModuleScope I use an enum without cases, that works like module namespace, where I define some module-specific structs and classes that are not likely to be used in other modules, like view models, strings constants, etc.
For example:
extension PostsFeed {
enum Strings: String, LocalizedStringKeyProtocol {
case followActionTitle = "Follow"
case unfollowActionTitle = "Unfollow"
case friendActionTitle = "Add friends"
case unfriendActionTitle = "Remove friends"
}
}
ModuleConfigurator
Module may have several configurations. ModuleConfigurator's main responsibility is to pick the correct V, I, P, R implementations corresponding to the selected configuration and instantiate them correctly, passing all the required dependencies.
Module Builder
That's a tiny reinvented wheel, that is responsible for getting ViewController from Stroyboard if needed, and putting VIPER components together.
Dependencies
I do not use any DI framework, like Swinject now. Dependencies are always passed explicitly as constructor parameters to I, P, R.
In fact, it's only Presenter and Interactor that usually have dependencies.
And it's only Interactor that may depend on Services.
Presenter my sometimes have some presentation config or some delegate as input.
Service oriented architecture
I put all server API calls with response mapping into objects inside services. All other compicated things, like camera capture, video/images resizing/processing is also placed inside services.
Services that allow to get some objects from server are never messed with storage. You can get objects from server and then decide what to do with it. Store with StorageService or use as it is.
The application has the only ServicesContainer that is instantiated on the very top of the App launch and defines the set of services the app will use.
Building modules routine
Building process for each viper-module works the following way:
By default, the main ServicesContainer is passed to every ModuleConfigurator. ModuleConfigurator chooses the set of V,I,P,R corresponding to currently choosen config, takes necessary dependencies form ServicesContainer and passes them to V,I,P,R constructors. In fact, services are passed only to Interactors because it's only Interactor that may depend on services.
In case of need, I can pass any other container to any ModuleConfigurator explicitly.
I can also do without ModuleConfigurator, and pick V,I,P,R manually, explicitly passing needed services and dependencies.
Why so complicated
It may sound complicated, but in fact it's not.
I've added some sugar, allowing to build modules with default configs, passing default ServicesContainer, defined on top of App.
Building of the module with default config with default ServicesContainer just takes a few lines of code.
func routeToWalletAcivity() {
AppModules
.Wallet
.walletActivity
.build()?
.router.present(withPushfrom: self)
}
I can specify usecase config and pass the exact ServiceContainer to it.
func routeToPostEditFor(_ post: PostProtocol) {
let configurator = PostsFeedModuleConfigurator
.editPostConfig(AppModules.servicesContainer,
editPost: post)
let module = AppModules
.Posts
.postsFeed(.singlePost(post))
.build(configurator: configurator)
module?.router.present(withPushFrom: self)
}
AppModules
AppModules is enum that works just as a namespace where all available app modules are listed and separted to different domains of the app as nested enums.
enum AppModules {
enum Posts {
case postsFeed(PostsFeed.FeedType)
case comments(PostProtocol)
case upvote(UpvoteDelegateProtocol, Upvote.UpvotePurpose)
case upvotedUsers(PostProtocol, UpvotePickDelegateProtocol)
case donate(DonateDelegateProtocol, [BalanceCurrency])
}
enum Wallet {
case walletHome
case walletActivity
case walletActivityContent(ActivityCurrencyType)
case walletPayBill
}
}
Great solution to the problem when you get to a new on-going project and have no idea which screen is where, what is done and what is not done.
Profits
Repeating the original post
Reliability. This way of building modules almost guarantees correct instantiation. Everything is checked at build-times. It allows to avoid common mistakes, caused by forgetting to pass some config or dependency.
Rather useful when the amount of modules grows up to dozens.
One of the weak places is Storyboard view controller IDs. Unfortunately we cannot check it during build time. I use codegen via Xcode template that inserts correct IDs for each module's Storyboard. That helps in some way.
Flexibility.
Such approach has different levels of flexing the code.
In my current project, one of the most complicated modules (posts feed screen) is having:
- 1 Interactor with 9 content type filtering and request settings (different kind of feeds)
- 2 presenters for compact posts presentation and extended.
- 6 implementations of ViewConrollers. Half of them has much in common and share some base implementation, the other half is rather unique.
Another example is custom camera module, that has:
- 1 Interactor
- 1 presenters
- 4 implementations of ViewConrollers: photo, video, combined video + photo in the same screen, QRCode scanner
T.G.I. VIPER, otherwise it could be an incredible pain to maintain and implement new features.
Module API Protocols that cover each module component are like small packages for shit, that keep the whole shit organzied.
Being a startup we apply a lot of changes constantly, VIPER works like a strong basement, making overall code structure still and at the same time extendable.
Unidirectional data flow
This idea that is widely used in Web development and is quite simple:
Data should flow in one direction through your application, like shark, that can only swim forward.
On practice that means, that you should keep the state at the bottom, and have one data flow possibly ending in changing the state. State change produces another data flow, that goes up and updates the UI.
In fact, VIPER doesn't strictly force to follow unidirectional data flow by design, and it is something we should force ourselves to do. Keeping state at the bottom in Interactor only helps us a little bit.
Assuming I'm using CoreData, most of the time Interactor keeps the state via FRC that triggers updates. Sometimes it keeps some plain, non-managed object. Anyway, ViewController and Presenter are stateless and it makes the code less exposed to becoming a spaghetti.
Instead of conclusion
4 months has passed since my previous post about VIPER and I'm still pretty satisfied with this approach. The project is 1 year old now. 1 year of heavy development and the codebase is still feeling rather good.
Comments