Every system and architecture tackles the test of time. It's an unfair battle, almost impossible to win. That doesn't mean we should surrender and hack unsustainable components without the ambition of longevity. We must develop them to deal with this challenge gracefully and try to defer the inevitable as long as possible.
Nothing lasts forever, though, especially in a fast-moving industry like mobile app development, and some missteps are bound to bubble up along the way. Let's define the most common and explain possible ways to avoid them.
Overly logical view
Let's start with an obvious one, an arch-enemy to any solid architecture—a view that does way too much.
The view is an excellent, self-describing name for a software component. Its purpose is to view what's happening inside the system in a user-comprehensive, preferably friendly way. It renders to the screen, listens for content updates, and delegates events to someone else to handle.
When there's a need for an extensive user-interface redesign, it's the view's role to step up, take all the heat, and keep all the vital bits of the application unchanged. It's a straightforward process, as no important presentation or business logic gets in the way.
That's an example from an optimal world where the view is a thin layer that does just the minimal job it should. Yet some programmers complicate the layer with additional logic, like persistence, state management, or navigation. Those should have their foundation elsewhere, and it's no surprise that dealing with all this extra complexity might prolong redesign estimates from a few days to a couple of months.
As system and platform designers, Google or Apple aren't leading by example either. On the contrary, they encourage programmers to place too much weight on their views' shoulders by providing them with components that require such an approach. One example for all: the FetchRequest property wrapper, defined in SwiftUI, fetches entities straight from the Core Data store. To quote usage from docs: "Use a FetchRequest property wrapper to declare a FetchedResults property that provides a collection of Core Data managed objects to a SwiftUI view." Ironic slow clap for encouraging programmers to couple views with data storage.
There are plenty of wolves in sheep's clothing, so keep your guard up and remember—just because you can doesn't mean you should. Your view should preferably not do much. The thinner the view layer is, the better. UI tends to change quite often, and keeping the view layer lean is a good practice for making swift and safe changes.
Almighty view models
As part of the presentation layer, view models are responsible for formatting content for the view and handling its events. It should be a comprehensive component, a little helper for the view, listening for changes, and handling presentation logic that lightens the load for the view.
Usage varies per project, but typically, it observes an application's state, transforms it into user-presentable data, and notifies the view. Unlike views, they are independent of UI frameworks, which makes them easily testable. That's not just icing on the cake but a massive step towards robustness.
However, they are often misunderstood and create a cesspool for what didn't fit elsewhere. So it happens that they handle business logic, navigation, and all other bits that don't have an official place elsewhere. Some scoundrels even compose requests and deal with networking! That's wrong on many levels; it defies the separation of concerns and tries to fit multiple components into one. This misplaced logic is not reusable and complicates further development on top of that. Such view models span hundreds of lines and compel us to recall massive view controllers from the Objective-C era.
Overly bloated view models are often present in architectures designed by proponents of MVVM who, in naive pursuit of simplicity, argue that three-layered architectures should easily cover all needs. Simplicity is, without a doubt, one of the goals, but the multi-hundred-line view model is anything but simple.
MVVM is not an architecture. It's a pattern describing how to structure the application’s presentation layer. It would be unwise to build a complete application around any pattern. Why should MVVM be an exception?
Should you find yourself struggling with where to put something in codebases where the MVVM pattern serves as an architecture of the whole system, you might be on the right track. Navigation, networking, business logic, and many others don't have a well-defined place in incomplete architectures. You can lift the burden from view models with proper domain modeling, define a spot for all those other concepts, name them, and be one step closer to a clear and comprehensive design.
Lost in navigation
In the early days of mobile application development, we didn't emphasize navigation much. Widely adopted architectural patterns such as MVC or MVP didn't have a letter in their name designated just for navigation, so we solely used system components such as UINavigationController, Activity, and Fragment, respectively, Storyboard segues. But these are massive system frameworks mainly “designed” for UI presentation. Their basic use complicates the view and further couples with the interface builder (used for scene designing) in the worst case.
Fortunately, things have gotten a lot better in the last few years, and with architectural enlightenment have come plenty of new designs. Some have even had their opinion or designated components on how to handle navigation. That's how coordinators, wireframes, flow controllers, and so on came to be. However, extracting the navigation into its place has revealed issues hidden in plain sight, such as sharing data between the screens. Some architectures do address this, but it is often neglected or misunderstood, so state propagation becomes another navigation responsibility.
The state dramatically complicates the situation for everyone involved in navigation. It must be obtained from the call site and passed to the next scene, which decides where to store such an incoming state. And it doesn't just end there. Once you wish to return to the previous scene, you need to take the state back with you and decide whether or not to replace the last one. That doesn't sound very easy, even for small-scale apps; imagine making sense of this in an app with hundreds of screens.
When you take a step back, however, you will notice that it is not so much a question of navigation. The problem is in state propagation and its coupling with navigation. Decoupling state to its place significantly improves usability, testability, and simplifies navigation. It also makes it clear as to who owns the state and where the source of truth is.
The post might seem to focus more on a separation of concerns than architecture, but those two go hand in hand. Once you realize the bloated multipurpose components are too hard to tolerate, you start to look for something more cultivated than three-word patterns dressed as architectures. More layers do not equal more complexity. They bring order, and clarity, and leave no place for presumptions (which lead to unwelcome pull-request surprises and heated discussions).
The resulting product is not the only and most important value we produce. By making the software genuinely soft, we can deliver continuously, with the same effort, and in the long run. That's an essential feature valued in any field by any business.