Surely there must be the best way to build an iOS app? Just search the internet for how others do it, or ask a professional.
Model-View-*
The first thing you may hear about and probably start with is a family, or rather an evolutionary chain, of MVC, MVP, and MVVM. After all, Apple officially recommends MVC for UIKit, since it’s well known across the entire software development world, fairly simple, and separates logic from the UI. MVP then separates it a bit more; MVVM still more with a slight twist to it. I’m not going to describe the architectures as others have done a splendid job of it already. For example, this timeless article by Bohdan Orlov or this fun take on the essentials.
What I’m going to do is comment based on my experience.
MVC
Makes sense for very small apps or experiments in UIKit. If the app starts to grow, it’s up for a rewrite. Not relevant for SwiftUI much, because there’s no “controller” anymore. UI is mixed with logic by default, so the starting point for SwiftUI apps is the “big ball of mud” architecture, which is fine for a prototype. Or you could look into the Atomic Design (see below) to structure your view components better and place logic in them strategically. After all, a SwiftUI view doesn’t have to render anything of its own. It can contain just logic and compose other views based on it. Akin to what React.js apps often do.
MVP
Mostly superseded by MVVM.
MVVM
Probably the most popular option out there. In combination with the Coordinator pattern, a.k.a. MVVM+C, this is a great starting point for any professional app. If the complexity grows, it can be scaled using additional software design patterns and separating concerns further. Also works well with SwiftUI: All you need is ObservableObject.
While MVC and MVP strive to separate UI from logic, having display logic still mixed into the “logic” part, MVVM takes a step further. It separates view (UI + display logic) from the application logic. The so called View Model (a terrible name) has no notion of UI elements. It doesn’t reference any view, doesn’t enable any buttons, doesn’t fill in labels… It deals with pure logic: Fetches and formats data (for a display somewhere on screen), initiates tasks triggered by the UI actions, and contains some business rules, in a nutshell. It contains a state for a view. The view reads the state and adjusts accordingly. Very useful for unit testing.
However, the definition of “the application logic” in this case is quite general. It’s everything not UI related. MVVM doesn’t help much there. View is separated, good. The rest can tie itself into a knot as far as the architecture is concerned, or rather unconcerned: Networking, data persistence, external events, form validation, sharing state, dependency injection, business rules… Do whatever you like. If the logic starts growing over your head, take a look at the Clean Architecture.
Note: I’m avoiding the term “business logic”, as it may be misleading. Decisions on which REST endpoints to call and when, or which data to cache, for example, have nothing to do with business. In that sense, “business logic” could mean domain logic. Something end users are familiar with. Oftentimes though, it is used as an umbrella term for the application logic. I admit, the “application logic” isn’t that much better either. Is it all logic found in the app? What about displaying views? There are many kinds of logic. Still, I hope the context here is clear.
Difference between MVP and MVVM
If you look at the diagrams often offered when explaining the two architectures, there is no difference. The concepts are quite similar. The devil is in the details. And there’s a fundamental distinction.
In MVP, the Presenter drives the view. It has a reference to it and manipulates it on updates. It doesn’t only calculate the text to be displayed in a text field, for example, it also applies it to the component, albeit through an abstract interface. The Presenter is the View Controller split in two, taking care of more general parts of its responsibilities. When unit testing, the view has to be mocked in order to observe the output.
In MVVM, the View Model knows nothing about the view. It exposes a state that is modelled roughly for the view’s purposes, but it doesn’t manipulate the view. It signals the state via reactive properties. So it can provide something like isSavingEnabled property, but it doesn’t enable or disable any buttons or add asterisks to the title. It is, in fact, an information model of a screen that can be rendered in different ways. The outputs can be observed directly during unit testing.
MVI
Model-View-Intent is more at home on the web and on Android. It sort of marries the UI/logic split and observable store concept I write about below, so it belongs to both camps, I think. It’s a question whether it does it well and whether it provides any additional value to the more mainstream alternatives. The one you should answer for yourself. I don’t have much experience with it and there aren’t that many good resources about it. The few original articles that proposed the idea are referenced here. Myself, I’m not too intrigued.
The earliest mention I could find is by André Staltz on his blog, who was probably at the inception since he used the architecture for a JavaScript framework Cycle.js.
Later it was adapted by Hannes Dorfmann for Android.
To me it gives an impression like MVP and Redux had an affair and produced this unusual child by surprise.
The Clean Architecture
It all started with this book by Uncle Bob, and this blog post. The main ideas are separation of concerns and one-way dependencies.
I’d say more important than architectures sprouting from the concept — famously VIPER and VIP — are the guiding principles.
VIPER
VIPER goes beyond the separation of UI/not UI and actually addresses the logic part more thoroughly than the MVVM does. It’s a solid example of a practical application of the Clean Architecture. Here’s an introduction by the authors at Mutual Mobile.
It’s a good fit for larger apps and it scales well. It forces certain structure.
If not approached dogmatically, but seen rather as an inspiration, it can guide an evolution of an MVVM+C app. View Model would replace the Presenter and would be further split into Interactor, a.k.a. Use Case, that would take care of some of the logic related to data marshalling and domain logic (business rules). The Interactor would communicate with services, leaving the state for the View Model. Interactors would support reuse of operations within a module, as they are not tailored to a specific view. They could be called from anywhere within a scope. Easy to shuffle functionality around different screens. As a bonus, View Models don’t need protocols, unless you want to unit test views, which are better covered by UI tests. Only views depend on View Models.
Coordinators would already handle navigation, so no need for Routers. Assembly of units could be done by DI containers, which are better suited for the task anyway and can handle also other dependencies.
Additionally, the Repository pattern is indispensable in governing the data queries and converting between domain and persistence data models. (The format of the data stored and used by the app logic is likely to diverge eventually. It’s a good idea to model them separately from the start.)
VIP
Another take on the Clean Architecture. Not sure how it meshes with SwiftUI. I suspect that if adapted, it would end up as a variant of an observable store solution (see below).
Observable Store
There’s a category of architectures that promote separation of the app state into a standalone, passive store and unidirectional data flow. Since the main feature is the independent state, that is essentially a big data structure with no logic of its own that changes constantly and emits notifications about the changes, I call the concept as “observable store”. It fits naturally with state-driven libraries like SwiftUI or React.
The allure of the concept lies in the idea of a centralized state that is easy to examine, persist, and test at any point in time. It is a data-complete description of the app; the rest is just a visual representation of it. The unidirectional data flow makes it easy to follow the UI updates. UI emits an action, that triggers some logic that lives in a separate place, eventually the logic updates the state, UI listens to the state and updates itself. No complicated ping-pong between UI and logic, no matter how complex and interconnected the UI is.
The price to pay is more difficult traceability of code. Normally, there is a method that calls another method and that calls another method… Here, an event is emitted and that is handled somewhere by something, hopefully, hopefully just once. The logic can emit also other events. Prepare to hone your search skills to extreme. Scoping and sharing state needs to be approached very carefully, too. As anything can theoretically change any state if you lump it all together in a single store. If you structure your code neatly and consistently, maintain discipline, these are not big problems. It’s easy to make a mess, though.
The Composable Architecture
TCA for short. Very scalable, quite promising, quite popular. A decent alternative for MVVM enhanced by the Clean Architecture. You can learn more about it in a Point-Free tutorial and on GitHub.
Redux
Originally created for React web apps. Difficult to navigate the code, messy side-effects. ReSwift is a prominent implementation for iOS, although the project doesn’t show much activity anymore. There are others. SwiftRex, for example.
MVI
Mentioned already above.
Atomic Design
Atomic Design focuses on structuring UI in a reusable manner and that drives hoisting application-specific state and logic up. Although the AD doesn’t meddle with logic per se, it ties in nicely with the stateful vs. stateless component concept, or container-presentational pattern. If you want to treat your SwiftUI app as mostly UI with a bit of logic sprinkled in, this is the way to go. But these principles are also applicable to any app that wants to structure its UI with less pain involved.
Common Topics
All these architectures try to structure code in a “better”, ultimately more efficient, way. “Better” can mean different things to different people. So rather than bounce around fancy names and expect a revolutionary concept in every proposal, let’s examine the recurring underlying topics they aim to address.
Separation of concerns
Is about splitting the code to smaller parts to be able to reason about them easier. Less concerns, less circumstances we need to consider, simpler to make a change. Units are more focused. Spaghetti code is harder to write. It basically reduces time to understand the code locally and to make a change.
Lack of any separation is referred to fondly as “big ball of mud”.
Every architecture mentioned above addresses the separation to a varying extent.
Testability
Breaking down components to small enough and relatively simple units that can be covered by relatively simple unit tests. It’s much, much easier to robustly test tiny pieces of logic a hundred times than write a couple of tests to verify a component with a hundred of gnarly conditions. It’s easier to maintain, too.
Exposing internals for testing without making them a part of API can be tricky without proper decomposition. Have you ever seen comments like: Don’t use this method! It’s just for testing!
Code covered by unit tests is more reliable as it has been re-examined carefully after the implementation. It also provides a regression safety net against continuous changes. Not mentioning benefits of treating tests as usage documentation. Bottomline: Fewer bugs, duh.
MVP, MVVM, and VIP take a step towards better testability. VIPER and observable store architectures, especially TCA, take more steps.
Data flow a.k.a. who calls whom
Even with a good separation of concerns it’s possible to weave some spaghetti. UI calling logic, which calls back to the UI, that in turn calls to navigation and passes some dependencies provided by the logic, navigation updates data storage… Sounds twisted? Add cascading changes to the mix.
Most architectures are concerned with the data or control flow. They offer lovely diagrams of it. Unidirectional flow got a lot of attention lately. Meaning a unit sends events to a single other unit and listens to another single unit, so units are connected in an ouroboros. It’s easy to reason about, because inputs arrive from one place and outputs depart to one place. There’s no coordinator or controller. Observable stores (TCA, Redux, MVI) follow this pattern.
Why would you care about the data flow? To have a clear answer to what caused which changes on screen. With one source at a time, run-time changes are much better traceable through the application.
Transparency
What’s going on in the app at any given time? In order to debug efficiently, we need to examine the app’s state. To enable a straightforward examination, it makes sense to extract the state from various components and centralize it in a store. That store can be persisted, recorded, and observed. Mutations of the store can be regulated and actions that lead to mutations can be recorded and replayed. Following this concept, we can have a detailed insight into the app’s internals and even capture tricky scenarios.
This is supported by observable store architectures (TCA, Redux, MVI).
Managing state complexity
The state can be modelled differently and can get very complex. Multiple properties can pertain to one state and mutate together. Some transitions may be invalid. If you look at the state as a plain data structure, there can be a multitude of variations in its values. In reality however, the combinations are limited. That’s where finite state machines (FSM) come to the rescue, to manage that complexity, to make possible states and transitions explicit.
TCA makes use of FSMs. There are also separate frameworks like ReactiveFeedback and CombineFeedback that can be plugged in. There’s a good article by Vadim Bulavin that showcases the CombineFeedback in action.
Careful though, on a small scale it could be an overkill. It could result in a boilerplate full of switches that is hard to read. You may live happily with enums and good old methods instead of events in your view models.
Reusability
There’s certainly an appeal to reuse UI elements or pieces of logic to shorten the development time and avoid bugs by utilizing mature software. Not many architectures address this need though.
In VIPER, Interactors could be made for reuse to an extent. Atomic Design, TCA, and MVI are designed with reuse in mind. Observable store architectures in general are well decoupled and can facilitate reuse of logic more easily.
Top comments (2)
With the introduction of SwiftUI, MVVM has gained popularity.
MVVM has been popular with UIKit, too, as far as I remember 🙂 It’s been around for quite some time and can be well integrated with many UI frameworks. With SwiftUI, I actually started to hear more opinions that we don’t need View Models anymore.