For developers, the most exciting thing to come out of WWDC 2019 was the introduction of SwiftUI. It’s a new user interface framework that is in turn based on new Swift 5.2 language features and a new Reactive Programming framework called Combine.
SwiftUI incorporates many ideas that have been percolating around the development community for some time. These ideas are themselves refinements of long-standing software engineering principles.
Using these ideas results in cleaner code, fewer bugs, and better performance.
However, SwiftUI is only available in iOS 13 and above. And based on Apple’s track record it will probably be a while before it is “ready for prime time” – the tooling and implementation is usually buggy and slow for early releases of Apple technologies.
But that doesn’t mean we have to wait to put into practice the ideas behind SwiftUI. The concepts can be applied to any code, in any language. (Just like you can use Object Oriented principles in C.) Ideas require no third party frameworks, just your brain and a plan and some architectural standards.
If you only watch one WWDC 2019 video, I recommend Data Flow Through SwiftUI because it goes beyond the specifics of Apple technology and touches on greater issues of Software Architecture. Yes, there are some technical details, which rely on prerequisites from other videos or tutorials, but it’s not essential for this discussion that you follow all of that. I will focus here on a few of the key lessons in that very dense presentation – lessons that don’t necessarily depend on SwiftUI.
In keeping with that, this post will stay at the 1,000-foot level and avoid implementation details. In future posts I’ll explore how to implement these ideas in code, with or without Combine and SwiftUI.
A quick note: if you’re already using a Reactive Programming framework like RxSwift, you may already be familiar with these concepts. I recommend to keep using your current framework for now, but it still can’t hurt to step back and remember the core goals. It’s easy to get caught up in the mechanics and forget the why. If you’re not using an RP framework, I would hold off on incorporating one until Combine stabilizes and you no longer need to support iOS versions below 13.
Carefully understand your data
The central task of all programs is to work with data – read, modify, write, and present data to the user (which is a special case of writing it). That same logic applies to each component of a large program – each button, each view, each section of the application performs those tasks on data.
Every piece of data has a “Source of Truth.” That is, there is one component that “owns” the data. If a piece of data is duplicated in more than one place, that’s an opening for bugs to creep in, and you should rethink that part of your architecture.
In classic MVC-based patterns, the Model layer holds the source of truth for most data. But as we have seen in the proliferation of MVC variants and refinements, the way this truth is stored and distributed can vary quite a bit.
One of the newly popular trends is Reactive Programming (abbreviated as RP). There are many sources to learn more about RP, so I won’t go into depth here. At the highest level, it can be thought of as a generalization of the Observer Pattern – your one source of truth notifies its observers whenever the owned data changes, and they “react” to those changes.
Combine is a framework for RP, and SwiftUI uses the RP features of Combine. Even if we don’t want to go all in on RP yet, we can achieve many of its benefits by using the Observer Pattern directly.
Minimize sources of truth
When you access a piece of data you create a dependency on it. Dependencies create complexity. By following simple patterns we reduce the complexity. Reduction of complexity is one of the core goals of Software Engineering.
As a simple example, let’s use an unread count for a social media chat app (one of the motivations attributed to the creation of Redux in React). You need to display the count in multiple places – in the chat icon at the bottom of the screen, the icons at the top, in the window title, and so on. It can also be modified in multiple ways – receiving a message, reading a message, or manually by the user marking messages as read or unread.
Without the concept of a single source of truth, tying together all the ways the count can change to all the places it needs displaying can become complex and error prone. If, however, we keep the unread count in a centralized model, things become much more manageable. The interface to the model allows two things: changing the value or subscribing to changes. So the icon with the unread count at the bottom of the screen doesn’t hold its own copy of that state, but observes the data model.
Views represent a state, not a stream of events
This is an important concept for wrapping our heads around the complexity of keeping a user interface consistent. If we try to think about every event that comes in, with all their different orderings, we make mistakes. SwiftUI handles the mapping of events to state changes. Without SwiftUI, we can still write our own code to perform that mapping. Although it won’t be as elegant or magic, even a few steps in this direction can help tremendously.
Use an update function
A good way to implement this idea in UIKit code (that is, pre-SwiftUI) is to write a simple update function in your UI component, which accesses the data the component depends on via the respective Sources of Truth. It then translates that data into configuration calls – setting the value of a text field, the color of a button, etc. All your observation callbacks simply dispatch to this single update function.
This updating happens behind the scenes in SwiftUI and other reactive frameworks. Conceptually, all dependent components are re-rendered whenever a data dependency changes. (“Conceptually” because the framework does a lot of caching and optimization, but you can ignore that most of the time.)
Dispatch modification events
The update function handles data changes that come from outside, but what about when the component itself needs to update data?
It’s important to remember that the component doesn’t “own” the piece of data. So it should not modify the data directly, but should dispatch the modification event to the actual owner – the Source of Truth. The data’s owner receives the modification event, makes the appropriate change, and notifies all observers, including the original component that triggered the change.
That flow of data is fundamental to managing complexity. Even though it may at first seem simpler to modify the data directly, that leads to consistency problems between components. Anything that modifies application state should be kept out of UI code as much as possible. This also provides for good separation of concerns, another solid Software Engineering principle.
The idea is to keep data flowing in just one direction, which always passes through the Source of Truth.
This unidirectional data flow will look familiar to anyone who has used Flux or one of its implementations such as Redux. But as you can see you don’t need a full formal Flux implementation to use its concepts.
When should a UI component own data?
Almost never. The data should only live inside a UI component (think UIView or UIViewController) when that data is specific to the presentation layer. An example would be the highlighted state of a button or an animation state.
Then who should own it?
This is of course the $64,000 Question. Where should your data live? If you ask 10 developers you’ll get 21 different answers.
The simple answer, of course, is “it depends.” Sometimes it’s obvious – for example, user preferences belong in persistent storage. I like to use Core Data, but for simple apps you may go with User Defaults. Or you may have your own database preference (Realm, for example).
Other times it’s not so obvious. Where do you put the results of a network request? Say, a list of search results. If the data is ephemeral, it doesn’t belong in persistent storage (ignoring caching for now).
Often this type of data will end up inside member variables of a UITableViewController. This is problematic because it results in tight coupling of the View and Model layers. And it forces you to keep that View Controller in the user interface hierarchy any time you want to manipulate or access those results. Despite its name, UIViewController components are part of the View layer in MVC. This approach is one reason many developers disparagingly refer to Apple’s brand of MVC as “Massive View Controller.”
The solution is to step back and rearchitect. One approach I’ve used with success is the Coordinator Pattern, for which you can find lots of documentation online. Similar or subtly different approaches go by names like VIPER, MVVC, and so on, but they’re really expansions of the classic Model-View-Controller paradigm, just broken down into smaller pieces.
Redux solves this by putting that type of data in a global “store,” while putting business logic into “reducers.” I don’t prefer this approach, but there are developers who swear by it. Just be aware that it’s hard to do Redux piecemeal; it tends to dominate your entire codebase, for better or worse.
The important thing is to get that data, and that business logic, out of your View layer.
Create reusable components
A side benefit of this separation of concerns is that it makes it easier to generalize your View layer components. A table view controller that displays apartment search results can easily be used to display tenant search results. If the view controller performed its own network request and stored its own data that would be harder to do.
This one is hard to discuss without concrete code, but basically, the data structures exposed from your Model layer to your View layer should be immutable. This enforces the restriction that the View layer shouldn’t modify the data directly. When the Model layer has a new version of the data, it sends it to its observers as new immutable objects.
For a short video, this presentation holds a treasure pot of valuable content, content that we can use not just for SwiftUI projects, but for anything involving data management and a user interface, which is almost everything.
This was a brief overview. In the future I will offer deep dives into some of these topics. If there is anything you would like to see me explore more, please let me know.