In the video ‘Data Essentials in SwiftUI’, published at Apple’s Worldwide Developers Conference, the phrase ‘Source of Truth’ is mentioned a whopping 31 times. So what does it mean? And how can thinking of this principle help you write better SwiftUI apps?
Where is our source of truth for data like user details?
What is the Source of Truth?
Source of truth is a key concept in SwiftUI, and if you watch Apple’s Worldwide Developer Conference videos, you’ll hear them reiterate this concept over and over. They are essentially saying you need to decide upon a single place to store a piece of data, and have everywhere else read the same piece of data — hence the single source of truth.
You should never have to try to keep two values in sync, or manually update one value when another one changes. The whole idea of SwiftUI is it is a declarative language, meaning given that the state is X, the view will always look like Y. If you give it the state X which is supposed to call function A to update state B, you’re writing code imperatively. This might be the way to write UIKit apps, Android apps in Kotlin or Web APIs in .NET, but it is not the SwiftUI way of doing things.
Let’s look at how we might display a list of items that we can sort in three ways, for example: by last updated, alphabetically, or randomly.
The declarative way of doing this sorting is by saving some state indicating which of the three ways we are currently sorting by is. Then, the view takes both the list of items to be sorted, as well as the state indicating how they should be sorted, and displays the list of items sorted the way the user has indicated. When the user clicks a button to change how the list is sorted, all it does it set the value of this sorted state, and the view will re-calculate (or re-computer, re-render, etc.) to display the data by the new sorting method.
This is as opposed to the imperative way of doing it, would directly call a sort method on the list of items as soon as you click a button. With this approach, there is no way of knowing which sort method we are currently using. Instead, we rely on the action taken by the user to sort the list. If we wanted to also display an icon representing which way we are sorting, we’d need to update the action that is called when the user changes the sorting method to also update the icon.
Let’s have a look at an example ListViewController I’ve made up for demonstration:
Each method updates the view. Even if we only call each method once, many different permutations result in different views. Take a look:
Different permutations of the same three methods called only once
Now imagine the many possible ways a view can be updated if we add just one more method, or repeat the methods. Working out what our view should look like gets complicated very fast. Add in events like screen rotation, network activity, or the app lifecycle, and your UI becomes very complicated to reason about. No wonder UI is so hard.
Instead, let’s take a look at a view that is written in SwiftUI:
Granted, this view doesn’t allow the user to take any actions — but what is displayed on the screen is entirely derived from its state. Instead of there being an infinite number of combinations that can affect how the view looks, we only need to understand two pieces of state — items and counter. Yes, this view could have a more complex state, but the point is we don’t need to worry about a series of events directly manipulating the view, and what order they are in. Instead, we tell the view what the state of the world is, and it decides how to display our data. And what is the state of the world? Well, it’s our data, our source of truth.
And now, our data directly drives our UI. Given the same data, our UI will always look the same. No longer do we need to call events in the right sequence to update our UI to look how we want it to — we just update the data, and we let the view respond to the data.
Where is our Source of Truth?
So know that we know what our source of truth is — the data that drives our app — the next logical question is where do we store it? Firstly, I like to store all the significant data in ObservableObjects. By this I mean, store any data that isn’t simple UI state (e.g. a toggle on or off) in structs that conform to ObservableObject. This is out of the scope of this article, but for more information about ObservableObjects, check out my other post.
Our source of truth should always be placed in the uppermost parent. That is, the view that contains all of the other views that may need the data. In other words, the first view closest to ContentView that needs the data.
So now the question remains, where do we instantiate our ObservableObject? Let’s use two real-world examples of data we might want to access within our app:
- A list of movies, and whether the user has seen them or not
- Whether the user is logged in, and what their name is
The first example is the simpler of the two. We can contain the data for the list of movies to two types of view. One is the list of movies itself, and the other is each movie individually. Take a look at some sample code:
The use of movies is limited to just these two views. It will not need to be accessed anywhere else in my app, at least in the current implementation of my app.
Because of this, our Movies object is created in MoviesListView, as you can see on line 2. This is the uppermost parent, as it is the View containing MovieView, which also needs access to the same data. We wouldn’t create Movies in MovieView, as we wouldn’t be able to access it from MoviesListView, and we wouldn’t want to recreate it for every single movie.
The other example is storing whether a user is logged in, and what their name is. Once again, I’m going to refer to what I said earlier. We should instantiate this data in the uppermost parent. However, for user details, this is likely to be used all over our app — in any view.
Given this, let’s go ahead and put it in the uppermost parent of the app — our App itself.
I’ve instantiated our UserDetails ObservableObject right at the top of our app, and then adding it as an environment object on the ContentView lets our entire app access any properties from UserDetails. This means any view in our app can tell whether the user is logged in, and what the user’s username is.
We can use this to show the content if a user is logged in, or if they are not logged in we can have a simple conditional statement to instead point them to a login screen. Instead of triggering an event like login() , we derive whether to show them the login screen or not by something like if userDetails.isLoggedIn { ContentView() } else { LoginView() }. This makes it much easier to understand all the different ways your app’s UI can behave.
So there you go — a quick dive into the world of declarative UIs and source of truth. Hopefully, this post gave you some insight on what your source of truth is, and then where to store it within your app. Because the UI is directly tied to your app’s lifecycle in SwiftUI, these decisions are important to ensure your data is there when you need it. By understanding what your source of truth is and where it is in your app, you’ll be able to make better decisions about where to instantiate your objects, which will make your code easier to understand and follow.