MVP-C iOS Modularization

Brenno de Moura 🏳️‍🌈
11 min readOct 23, 2021

--

The modularization of iOS apps is being increasingly demanded by companies and also to the scaling of projects, due to its optimization of team management as well as the build time. Modularizing an application involves many other things that need to be worked on to have an excellent effect on the project’s architecture, however, this article details only the UI part (View, ViewController, Presenter, and Coordinator) common to all applications.

There are several articles published on my page that abstract some implementation and specific approaches about the View or Coordinator, adding as baggage to help assemble more complex structures in an application. However, reading these articles is not mandatory and should not impact the understanding of this article.

Detailing the modularization of an application is quite complex and easy to lose your mind during design, so I haven’t been willing to do that here on Medium yet. I’ve come across a few projects, articles, and implementations that cover various concepts in a superficial and confusing way, and hopefully, this article goes in the opposite direction, allowing you to implement multiple projects using a modular architecture.

I confess that I don’t know most of the issues surrounding this implementation and I can break some principles, such as SOLID, Clean Architecture, or others. So, you are welcome to bring this discussion to the comments section, where we can better understand this topic and get to know other points of view.

Concepts

Modularization is nothing more than dividing an application into specific modules. Each module must have a purpose and access pieces of the application to be able to perform UI operations, as well as requests, validation, database access, and others.

It is very suitable for this subject to study and absorb the contents on the internet about Clean Architecture, Domain Driven Design, and other links that are available on the web and also on Medium. However, the concepts covered in this article are related to user interface modularization.

Every iOS app contains a UI layer that presents information and interacts with the user, performing transitions and animations in the interface. With this, the modules must obey a nomenclature to standardize and contextualize the parts, is recommended to use the Name + Feature format forming modules called LoginFeature, UserFeature, and others.

There are several ways to implement this layer and, for this article, the MVP-C architecture is used, where we have the Model, the View (ViewController / View), Presenter (or non-reactive ViewModel), and the Coordinator.

Protocols

The implementation of a modularized feature involves the definition of some protocols for each object, not being mandatory, it is important to implement the object unit tests and generalize solutions. Furthermore, the use of protocols limits the use of object methods and properties to those in particular regardless of who implements them or what operations they perform.

Each component of the MVP-C architecture must receive its abstract protocol so that it can be used without knowing the concrete object. The protocols are Viewable, Presenting (or ViewModelling), and Coordinating.

Each module must contain the Scenes folder with subfolders per screen. Screens must contain at least four folders: Coordinators, Presenters, Views, ViewControllers, and Protocols. As shown in Figure 1, each folder should contain as few files as possible, but it can rarely happen that there are separately implemented subviews that should be placed inside the Views folder.

Figure 1 — MVP-C modularized folder structure.

Another important point about protocols, and their correspondence to the architecture within the Scenes folder, is that a subfolder is not always defining a canvas. This structure can be exploited to define specific subfolders for cells or other views, not necessarily needing to have all four objects of the MVP-C architecture.

View Code

The use of view code for this article is essential, while the storyboard or xib code should be abolished. Projects that are structured in storyboards or xib present a very great complexity to adopt this style of modularization, being totally necessary to transcribe the ViewController in storyboard to Swift code.

The modularization style via protocol and restricted definition of the architecture objects allow making the View a component rendered by SwiftUI, where Presenter is of type ObservableObject. For this, it is necessary to use generic types for the View and ViewController that represent the Presenter and thus comply with Swift’s rules regarding the associated type.

A good view code implementation removes the direct use of native or library constraints, requiring only the use of margin constraints with constant or zero offsets. In the case of SwiftUI, this comes naturally to the use of VStack, HStack, and ZStack. For UIKit, it is mandatory to explore the use of UIStackView.

FlowType

FlowType is a protocol that allows communication between Coordinators in different modules intermediated by the App. However, there must be other ways to integrate these components using other structures that allow the instantiation of Presenter, ViewController, and Coordinator with constant or non-optional properties.

In the context of modularization, the use of delegates between Coordinators or between View and ViewController can be found. However, this approach can increase design complexity and not fit in with SwiftUI abstractions.

There’s an article that details the Coordinators’ use of FlowType and how autonomous it is to drive different flows. If you are interested in learning about this approach, the link is found below.

FlowType can be replaced by a Router or inspire the implementation of an even more complex structure. Its simplified and specific design came to fit the problem of communication between Coordinators and allow transitions between screens to be made.

View

The View represented by UIKit’s UIView or SwiftUI’s View protocol is where all layout operations take place. It has autonomy over its content not depending on the existence of the Controller.

However, the View implements its Viewable protocol only when in the context of a single component. When it is set to render a screen, the Viewable protocol must be implemented by the Controller.

This differentiation is important due to the technical operations that can be performed on each component. When in screen context, it may be necessary to access properties unique to the UIViewController by the presenter. In the component context, communication is restricted to the graphical component of the view as in the case of UITableViewCell.

View and Controller must be treated as similar objects respecting their differences.

Both the View and ViewController share the presenter and can access properties or methods directly. As shown in Figure 2, this streamlines the view code process and is also useful for integrating the presenter with the view, keeping communication straight and without using a third-party component or protocol to mediate access to properties and method calls.

Figure 2 — View implementation using view code and declarative programming.

The Controller must be able to call the methods of the encapsulated View to report when a list has loaded or when some condition has changed. Therefore, although the View in the screen context does not implement the Viewable protocol, it is common, in most cases, for it to have the methods that the Controller implements due to the protocol.

As presented in the view code definition, it is essential to use this pattern of layout development using code. To leverage this process, I recommend reading the second article below that defines how to make the view code process declarative and similar to SwiftUI.

Figure 3 shows the View implementation using SwiftUI and Presenter as the generic type. This approach allows you to bring the best of both worlds together, as the entire layout is rendered by Apple’s new framework and the Navigation or Tab Controllers are kept in the old-style UIKit.

Figure 3 — View implementation using SwiftUI and presenter by generic type.

Thus, by standardizing the View implementation in view code and, in SwiftUI, passing the presenter directly, there is greater control between those objects that have frequent communication either through user interaction or events from other layers. Also, direct usage allows for faster programming performance as the dependency on keeping a third-party delegate protocol up to date is removed.

ViewController

The ViewController in this context of modularization is further limited to performing specific layout operations and assembling the hierarchy of views via the loadView method. Despite this, it has a very important role for UIKit and there may be operations that must be performed from it.

Its use is not even removed in the case of a modularized application that uses SwiftUI to render the view. It allows UINavigationController and UITabViewController to be used with navigationItem and tabBarItem.

The UIViewController provides a consistent API that is still essential to complement projects.

In some examples detailed in articles by other authors, in the case of SwiftUI, the use of the UIViewController is completely removed and the UIHostingController is used directly on the Coordinator. It’s a valid option, but it doesn’t fit in all scenarios where the UIViewController instance might be required directly.

In a UIKit-only application, the UIViewController provides a seamless API with a consistent lifecycle that is exploited by many applications to load lists and fire events to change the layout. Also, by maintaining this approach, the Controller now has access to both the Presenter and the View directly.

As shown in Figure 4, the example of UserDetailViewController implementing the UserDetailViewable protocol to receive data updates generated by the Presenter is detailed. Because the Presenter is shared, the Controller assumes limited and specific responsibilities and is an object that mediates communication from the Presenter to View.

Figure 4 — ViewController implementation using the Viewable protocol and the presenter.

In case the Controller encapsulates a View implemented using SwiftUI, it is necessary to implement a view that transforms the UIViewController into UIView. Thus, as shown in Figure 5, when creating the UIHostingController in the loadView, the hostingInView method is called and the Controller’s view is assigned.

Figure 5 — ViewController implementation using the SwiftUI.

The more SwiftUI advances in abstracting the UIKit implementations, the less dependent projects will become on the UIViewController allowing in fact to remove this object altogether. However, the stability given when using the Controller to build the screens is still essential.

As a recommendation, just below is a third article that details the implementation of a Design System for color, spacing, font, and components for projects. In addition, the development of utility methods (Extra Components) to create a declarative view code API is also discussed.

Presenter

The Presenter is an object that concentrates all operations to obtain data and update information in the View. While the View and Controller act like the body of a car, the Presenter is the hood where all the gears that make the car work are concentrated.

In its simplest and most primitive implementation, Presenter contains a variety of code to get data, request permissions, show errors, and update the database.

When it comes to modularization, it’s possible to break Presenter into multiple UseCases that are responsible for a small part of the whole, like a cog. It is common in modularized projects to find a UseCase to validate the email field, another to get the data from some API and another to authenticate the user with Face ID.

These UseCase abstractions and code specializations are a great alternative that keeps the Presenter with few responsibilities while its functions (email validation and so on) are shared across multiple UseCases. Figure 6 shows the LoginPresenter implementation, making use of EmailValidatorUseCase and FaceIDAuthenticatorUseCase.

Figure 6 — Implementation of LoginPresenter using UseCase pattern.

This article does not explore the use of dependency injection. However, its use to store and instantiate UseCases can be a valid alternative that helps in sharing code between different Features.

With SwiftUI this implementation changes due to the idea that the View is represented by states and, even the presenter, ends up being forced to store and reflect the data to the View. Figure 7 shows what this same implementation would look like in SwiftUI.

Figure 7 — Implementation of LoginPresenter using UseCase pattern in SwiftUI.

An interesting point, in this case, is that the Viewable protocol ends up losing some functions to notify the View that something has changed. Since SwiftUI knows the Presenter state, when it changes, the View is automatically updated with the new values.

In addition, the Viewable protocol ends up being used to just notify the Controller of some event to call some exclusive UIKit or legacy frameworks method. This implementation ends up making it easier for Views that contain lists or, if necessary, implement a View unit to be coupled to another Feature or screen.

Coordinator

The Coordinator is an object responsible for managing screen flow. In the iOS community, this object can be implemented in different ways and have weak or just var properties depending on the team’s understanding.

The Coordinator’s default, and best known, implementation is by flow context. In an app, we have several flows that the user can go through, whether it’s to edit various profile information, or to buy a product, or to log in.

In the case of the login flow, we can find the LoginCoordinator within the Login Feature with all the options to advance in the flow, whether to open the registration screen or recover the password and other screens that make up the flow. With this, the Coordinator is shared among several Presenters and ends up having an amount of code greater than 500 lines with methods that overlap each other.

This approach also explores other concepts related to the Coordinator implementation, which ends up making the object to transition between screens quite complex.

The solution presented in this article involves implementing a clean Coordinator with few methods, just to call other Coordinators, and to mount the Controller on the device’s screen. This implementation is fully valid for adding with dependency injection and also for assembling View + Presenter fragments.

For the transitions to occur, a host is needed to perform the animation and replacement from one Controller to the other. We have UINavigationController, UITabBarController, UIViewController and UIWindow.

The host is referenced inside the Coordinator by the weak var rootViewController or weak var window being configured by init. In addition, every Coordinator has the start() function specified by the Coordinator protocol. Figure 8 shows the LoginCoordinator that exemplifies this implementation.

Figure 8 — The implementation of LoginCoordinator with a few methods.

With this implementation, we can add variables and pass it as a parameter when calling a Coordinator from the next screen, keeping non-optional data when necessary. Also, because View in SwiftUI is rendered in UIViewController and so screen, we can use this same code for Apple’s declarative solution.

Another interesting point is to use FlowType to perform screen transitions between different Features. It makes the coding process even simpler and allows you to dynamically drive streams through the application.

In the case of a Coordinator for a View in SwiftUI, we can combine that implementation with the Coordinator.Provider or Coordinator.Builder who is also a View. This implementation is recommended for when we have a View that is used on different Screens and Feature, and we want to keep it unique and reusable.

Figure 9 shows how a Coordinator is implemented for the SwiftUI View. We can use the Provider object to add the UserView to any screen. The integration of this object with the screens involves some adjustments and code to generate the desired result.

Figure 9 — The implementation of a Coordinator for a single View in SwiftUI.

Considerations

Implementing the MVP-C architecture in iOS apps involves several concepts and options. What is presented in this article is one more option to be discussed and known by developers.

Due to the sharing of Presenter between View and Controller (maybe the correct name would even be ViewModel 🤷🏻‍♂️), it helps in the implementation of UI objects without requiring another protocol to complement the dialog between these two objects. The inversion of the Coordinator concept, per screen and with the necessary parameters in the Presenter directly passed at startup, makes the data safer and more secure, avoiding the unnecessary use of if let or guard let.

I recommend studies on UseCases, modularization of other layers, and dependency injection to complement what was discussed in this article, enabling the complete implementation of an application.

I hope this content was relevant and the comments section is open for us to exchange ideas and share experiences!

Thank you so much for reading!

If you would like to contribute so that I can continue producing more technical content, please feel free to buy me a coffee ☕️ through the Buy me a Coffee platform.

Your support is essential to maintain my work and contribute to the development community.

--

--

Brenno de Moura 🏳️‍🌈

Software engineer with a passion for technology and a focus on declarative programming, experience in challenging projects and multidisciplinary teams