SwiftUI: Observables, View Hierarchy, and Putting Them All Together
Taking a peek at the power of ReactiveUI design using custom observable classes
SwiftUI is defined as being declarative and reactive. The former is what allows us to write out our UI, which we can do in a very clean and organized fashion. The latter is what brings our UI and data closer together than ever.
We’ve been working on an app that allows us to view our team and dive into the profile pages of each member.
The last features we implemented were the ability to expand/collapse profile details, as well as toggle whether or not the team member is a superstar.
However, since we were only storing superstar status to a view instance’s State
, we lost track of that value once we navigated away from that view. What would be great is if we could keep that value alongside the profile—that way, we could navigate to and from different Views and our app would remember who the superstars were.
Custom ObservableObjects
One way we can accomplish our goal is to make our Profile
class an ObservableObject
. This conformity gives our class the ability to have properties that can be observed (or watched) by subscribers. These properties would then be tagged with the Published
property wrapper, indicating that it is bindable and will publish changes to subscribers as they occur.
Normally, this would be as simple as having Profile
inherit ObservableObject
, adding Published
before our properties, and adding a new Bool
property for superStar
. However, because our class is also Codable
, we do have a few extra steps. We’ll go through them here, but if you would like to understand more of the reasoning behind the steps, check out this great article by Paul Hudson here.
First, let’s work through the steps I mentioned earlier, since they’re still relevant to us:
class Profile: Identifiable, Codable, ObservableObject{
@Published var name: String = “”
@Published var subtitle: String = “”
@Published var description: String = “”
@Published var profilePic: String = “”
@Published var superStar: Bool = false
}
As I said, normally this would be enough. By now, though, you probably see Xcode giving us errors about Profile
not conforming to Codable
anymore. Because our properties are wrapped, we’ll need to provide encoding and decoding guides for Codable
. First, let’s add aCodingKeys
enum to our Profile
class that will name our expected data:
enum CodingKeys: CodingKey {
case name
case subtitle
case description
case profilePic
case superStar
}
Next, let’s add our Decoder
helper. This comes in the form of an init
that takes in a Decoder
:
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
subtitle = try container.decode(String.self, forKey: .subtitle)
description = try container.decode(String.self, forKey: .description)
profilePic = try container.decode(String.self, forKey: .profilePic)
superStar = try container.decode(Bool.self, forKey: .superStar)
}
Lastly, let’s provide an Encoder
helper:
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(subtitle, forKey: .subtitle)
try container.encode(description, forKey: .description)
try container.encode(profilePic, forKey: .profilePic)
try container.encode(superStar, forKey: .superStar)
}
That’s it! Give your app a test run and make sure everything works exactly as it had been before. Looking over the changes, even though we had to account for Codable
, it’s doesn’t turn out to be that much extra work. And now, we have a class that can automatically be decoded and observed!
Connecting to Our UI
Now that we’ve made Profile
observable, we’ll make a few changes to our Views so we can utilize our new capability.
ContentView
The first thing to do is wrap our Profile
array with @State
. This may seem trivial (and exciting), but it’s important to note that this now makes a Stateful array that holds observable elements.
The reason for the extra explanation is that our List
does not take bindings, which means that each element will be just a Profile
, not a Binding<Profile>
. We want the latter if we ever hope to accomplish our goals. What we can do instead, then, is change List
to iterate through profiles.indices
, and then use each index to pull the bindable profile
:
List(profiles.indices){ index in
ProfileCell(profile: self.$profiles[index])
}
This seems like unnecessary work, but it preserves a List
's purpose to iterate through values, not bindings.
ProfileCell
Just below ContentView
, our cell needs to catch the Binding<Profile>
. We simply add @Binding
to our profile property. We also want to add $
where we pass profile
as a parameter in ProfilePage
, sending in the binding instead of just the value: ProfilePage(profile: $profile)
ProfilePage
Finally, we’ll need to head over to ProfilePage
. First, simply add @Binding
to our profile property. We then can also remove our superStar
property since we’ll be replacing it in our view with profile.superStar
:
self.profile.superStar ? Color.yellow
Toggle(isOn: self.$profile.superStar)
Run
We’ve successfully passed our Profile
down the View hierarchy! It’s now time to test our app. When the app runs, go into a profile, toggle Superstar, go back to the list, re-enter the profile, and check to see that the toggle state has been preserved. If it has, then we’ve succeeded!
Proving Binding Up and Down the Hierarchy
There’s one more thing we can do to prove we’ve successfully passed our binding up and down the hierarchy of views in our app. We can add a simple symbol that will indicate on our List
who is a Superstar and who isn’t.
To do this, let’s go to ProfileCell
. We’ll add an Image
next to profile.name
with one of 2 SF Symbols: star
or star.fill
HStack {
Text(profile.name)
.font(.title)
Image(systemName: self.profile.superStar ? “star.fill” : “star”)
}
Now, when we run our app, we should see stars next to our team members’ names (filled if they are a superstar, hollow if they’re just stars). When we run our test by toggling stardom in our ProfilePage
, we should see the changes reflected back on our List
! Hence, proof that we’ve passed our binding across the hierarchy of our app successfully (and added a nice visual feature, too).
Where to Go from Here
When I thought about doing this series, my desire was to introduce people of all skill levels to SwiftUI. I’m extremely excited about its potential, and it’s inspired many ideas. My hope is that, having gone through our demo app, you, too, feel inspired to create something of your own using SwiftUI.
There’s so much more we could cover and jump into. And believe me, I don’t plan on stopping writing articles on Swift and SwiftUI anytime soon. But as far as getting our feet wet and building a foundation, you should have more than enough to take some bold steps on your own to create some fantastic apps that you will have dreamed up.
Go forth, and create!
Tip for the Road
We’ve covered a number of topics during this series, and in many cases only scratched the surface. I’d encourage you to play with your own app ideas, but if you’d like to continue building on our demo app, consider what changes or additions you could make.
For an example, what if you hosted the JSON of profiles and their images on a website? How could you pull that data using URL
? Or perhaps you’d like to be able to edit the profiles and save it to the local JSON. How could you leverage SwiftUI’s Form
and Codable’s Encode
to accomplish that?
Give those a shot, and when you feel ready, hit Start a New Xcode Project and build something wonderful.
Editor’s Note: Heartbeat is a contributor-driven online publication and community dedicated to providing premier educational resources for data science, machine learning, and deep learning practitioners. We’re committed to supporting and inspiring developers and engineers from all walks of life.
Editorially independent, Heartbeat is sponsored and published by Comet, an MLOps platform that enables data scientists & ML teams to track, compare, explain, & optimize their experiments. We pay our contributors, and we don’t sell ads.
If you’d like to contribute, head on over to our call for contributors. You can also sign up to receive our weekly newsletters (Deep Learning Weekly and the Comet Newsletter), join us on Slack, and follow Comet on Twitter and LinkedIn for resources, events, and much more that will help you build better ML models, faster.