Understanding SwiftUI Data Flow
A primer on some key protocols and property wrappers
I’ve seen many people having trouble architecting their apps in SwiftUI — because it’s a totally new paradigm and because it has very little official documentation. I want to use this article to share my use case of the various property wrappers exposed by SwiftUI, to help the data flow within your application.
Even if it’s a bit outdated (because SwiftUI already received a lot of refactors), I can’t recommend the 2019 WWDC session about SwiftUI data flow enough. It was thanks to this session that I first grasped the full power of SwiftUI and bootstrapped MovieSwiftUI.
Data Flow Through SwiftUI - WWDC 2019 - Videos - Apple Developer
SwiftUI was built from the ground up to let you write beautiful and correct user interfaces free of inconsistencies…
You have to take a look at the SwiftUI data flow documentation from Apple, which is up to date and not bad at all. It somewhat lacks concrete examples, which is what I’ll try to provide in this article.
ObservableObject is a protocol that’s part of the Combine framework. To use it you just have to add the protocol to your model class, then mark
@Published any properties you want to be observed by SwiftUI within this model.
When to use it?
It’s a good protocol to use on your ViewModel, or your model directly if you don’t have or don’t need ViewModel. Basically, any object that needs to hold properties that you’ll directly use in your view should be marked/wrapped, in
@Published, within an
ObservableObject. You can think of it as the SwiftUI model base class—although it’s a protocol, not a class.
As you may have guessed, this property wrapper is to use in convert with your ViewModel classes which conform to
ObservableObject. It will wrap your object into a dynamic view property, allow SwiftUI to subscribe to your object, and invalidate its view body anytime some
@Published property in your model change.
When to use it?
Use it whenever you need to bind an
ObservableObject to your view. In other words, any time you need your view to be updated regarding changes in this object.
SwiftUI views, being value type (as they are Struct), will not retain your objects within their view scope if the view is recreated by a parent view, for example. So it’s best to pass those observable objects by reference and have a sort of container view, or holder class, which will instantiate and reference those objects. If the view is the only reference to this object, and that view is recreated because its parent view is updated by SwiftUI, you’ll lose the current state of your
@State a property wrapper that you’ll use a lot in SwiftUI. It creates a persisted value (persisted between view refresh). It’s important to understand, as stated above, that SwiftUI views are structs and they are value type — SwiftUI could recreate your view any time, for any reason. So by design, all your properties in your views are immutable and will be recreated any time the view is recreated — simply because the parent view decided to, for example. You can see it as your local view state.
So, if you want to create a local to this view, persisted value that you can mutate (which then triggers a view update), you use the
@State property wrapper. The added benefit is that SwiftUI will also subscribe to it and invalidate and refresh the relevant part of your view whenever it’s changed, from your view, or by binding.
As you can see in the following piece of code, the
TabView, which is the
Tabbar component of SwiftUI, takes a binding, and you can generate binding from
@State property by using
$. We’ll talk more about binding in the next section.
When to use it?
Any time you need to store and persist a state relevant to your view. You need to see
@State property wrapper as a view specific local data that need to be persisted. It could be the selected tab of a
Tabbar, or the text value of a
TextField, or the
UIImage value of an
Image view. Also a
Bool value controlling if a
.actionSheet is presented or not.
@Binding property wrapper is a way to create a two-way connection to a value managed by something else. Most probably it is a
@State from a parent view — this is the most common way of using it.
In the above code, you can see my
NotificationBadge component — picture it as a toaster displayed at the bottom of the screen for a few seconds when the user does some action.
Bool property is passed by binding. This means that when you create a
NotificationBadge view you need to pass a
Boolcontrolled by a
@State. You can create a Binding from a State using
$. You could also create a binding manually (more on that later), or create a binding from a
You can also mutate a binding value. For example, I could turn it to false from within my
NotificationBadge component using
isShow.value = false. This will mutate the
State, so it updates both the
NotificationBadge and the parent view state.
When to use it?
The example above is a good one.
Binding is often used to pass a value controlled and persisted by a parent view, and to invalidate your view when this value changes. It’s a good tool for
Toggle, custom components, etc. You can propagate changes and create relationships in a complex view hierarchy by passing a
@State as binding around, while ensuring that only one view persists its value.
Create a binding manually
You can also create a
Binding<Value> yourself. Apple provides a very convenient init method to do so. The power of this feature is that you could trigger action and side effects when the value is set and read it from a storage of your choice. In my case, I use it mostly to bind to a value stored in my
AppState redux store and dispatch the desired action when it’s set. Here’s an example of one of my context menus:
As you can see, I toggle the binding in my Button action, which will dispatch an action of my store, and update the binding value to the desired boolean value, which in the end will update my view. Clean and simple.
A dynamic view property that uses a bindable object supplied by an ancestor view to invalidate the current view…
@EnvironmentObject is a property wrapper, which you can use if you supply a
.environmentObject() to any parent of your current view hierarchy.
The object you provide has to conform to
ObservableObject, and if you provide the root view of your app, as in the sample code below, it will be available in any view if your app. This is a powerful tool if you want to have an object available during the whole lifecycle of your app.
In the case of the Redux pattern I’m using, I’m injecting the whole
store holding the
AppState, so I can access it in any view.
Then, in any view, you can use the
@EnvironmentObject property wrapper. Your view subscribes to it and updates its content according to the data you derive from your object.
In the code above, I use the property wrapper, so my view has access and subscribes to the
EnvironmentObject injected at the root view.
When to use it?
The above example is a good one. My store holds the models and data essential for my application, so it makes sense to have it always injected and available. My views will be updated as it updates.
This is basically a dependency injection system, so it’s also a powerful tool for previews and debugging. For example, I inject a sample store, so I can mock data without firing a network query.
You could also inject custom values for your UI — maybe a dynamic colors palette. Maybe also some database manager, which will publish results in objects he holds wrapped
There’re many many possibilities with
EnvironmentObject. If you’re familiar with other dependency object libraries on Swift you’ll quickly see the doors it can open.
If not, this is the perfect place to have some sort of persistence for the models you use across your app, instead of, for example, a singleton or a global variable.
I hope this article sheds some light on the big topic that is SwiftUI data flow. While UIKit was not doing any magic on that topic, SwiftUI provides a lot of tools, and, in a way, it’s way more “on rails” than UIKit. It’s a bit harder to grasp, but once you start to get proficient with it, your apps will work and update their views like magic!
In my opinion, it also enables developers to spend much less time architecting their models layer, and more time on the UI. You can remove a lot of spaghetti code, manual binding, notification, delegate, closure completion handler, etc if you use the tools the right way.
Thanks for reading!