This article is an attempt to help beginners, like myself, understand fundamental things about SwiftUI. This is the type of article I wish existed when I was just starting. Hope it’ll save you some time and struggle.
Feel free to leave a comment with corrections or general feedback.
In order to really understand state management in SwiftUI, and in order to follow along with this article, you need to know a couple of things:
- Difference between value type and reference type
- What property wrappers are and roughly how they work
If you don’t feel confident enough about those topics, please take a moment to read these documents:
Otherwise, let’s dive in.
On a very high level, every app is a pipeline that takes some event as an input (user gesture, system notification, timer, etc.) and produces an output, most often by drawing something on the screen.
Different frameworks have different intermediate steps between input and output. In SwiftUI, the pipeline looks roughly like this:
Take a look at the Re-create Views step in the pipeline. By Views, I mean our custom
structs with the
body property, just to be clear.
It’s one of the crucial points when it comes to understanding the application state in SwiftUI. A lot of other concepts are direct consequences of this fact:
Views are disposed of and re-created every time an event occurs and the observed state changes.
This means we cannot reliably keep any data within our views. We need some other (external) place to store the application state.
Luckily, SwiftUI has built-in storage outside of the view hierarchy which we can use: SwiftUI State. This state is managed entirely by the framework, and we access it only through the provided API.
From the pipeline, you can also see that the only way to make SwiftUI re-create our views and, eventually, to re-render the screen is to update the observed data.
This means that we, somehow, need to tell SwiftUI to look for changes in some properties of our view and when they occur — re-create the view.
So, to recap:
- We need a way to put data in SwiftUI state which lives outside of our views
- We need to tell SwiftUI to observe changes in some properties, so the framework knows when to re-create views and re-render
@State property wrapper solves both problems at once. Let’s look at the simple app and its state layout.
@State creates an observable container for our property and puts it outside of the view. SwiftUI now keeps the connection between our view and the observed data. So that next time this view is re-created, it can inject the data into the corresponding property of the view.
Counter view has
Every time the Random button is pressed you see a message in the XCode console, meaning the
Counter view was re-created, but the
count keeps the same number because it’s stored outside of the view and injected every time the view is created.
It’s worth mentioning the distinction between SwiftUI disposing of an old version of a view before creating a new one and your code intentionally excluding a view from rendering.
In the latter case, SwiftUI will notice that the view is not a part of the rendered screen anymore and will release all the values it kept for this view in the state. The next time your code decides to show the view again, its properties will be put back in the state with their initial values.
Here is a demo of this effect.
I try to regularly post useful dev content on my Twitter, feel free to follow me there 😉
A common misconception is that you cannot use
@State with reference types like classes. You definitely can, there is no technical limitation to do it.
You just need to keep in mind that the property you wrap with
@State will hold a reference, and a reference will be the only thing SwiftUI will store in the state and observe for changes.
Remember that if you update a property of a reference type, its reference does not change. So to SwiftUI, it will look like nothing happened.
@State does not care what to wrap with an observable container and put in the state, it’ll do it just the same way it does for value types.
If you try to run this code you’ll see that it does not work.
Here, I’m updating the
count property by using
increment() and it updates as it should. The only thing is that reference that lives in SwiftUI state does not ever change, so the view is never re-created, and the screen is never updated.
Logically, to fix this example we need to update the reference every time we hit the button.
This works, but in most cases, it’s not quite the solution we really want. Re-creating an instance every time might be wasteful in a lot of cases, not to mention that code written this way does not communicate the intent really well. We want to increment the count on a counter, not a brand new counter every time.
These three play together to give us a way to change individual properties of a reference type and still communicate changes to SwiftUI, so it knows to re-render.
ObservableObject is a protocol you adopt on your reference type and
@ObservedObject is a property wrapper for a view property, which holds a reference.
@ObservedObject on a property, you are basically saying to SwiftUI to go and look inside the object that the property holds a reference to. Look inside and subscribe to changes from properties that are marked with
@Published wraps a property on a reference type with the observable container, just like
@State does it for view properties.
This is how it looks in the code of the Counter app.
Now it works with just calling
increment(), without re-creating the counter every time.
Unfortunately, this approach has a flaw and you can see it in the image above. SwiftUI State is empty.
@ObservedObject does not put data outside of the view as
@State does. And in case an object is created within a view, every time the view is re-created the object will be re-created as well.
Here is a demo of this effect.
Notice how the counter at the top resets every time the Random button is pressed. When we update the
random property, SwiftUI executes the body of the
ContentView and re-creates
CounterView along the way. The newly created
CounterView ends up with a fresh instance of the
Counter every single time.
This is where
@StateObject can help us out. It does all the same things as
@ObservedObject but additionally puts observed properties into the state outside of the view hierarchy.
Below, watch a demo with the Random button, where it uses the
@StateObject on the
counter property this time.
It might seem that we just need to use
@StateObject all the time and forget about
@ObservedObject. But there is a very good use case when you need both.
@StateObject in the view which creates an object. Use
@ObservedObject when the view receives objects from outside.
Consider this example:
Notice that even though two views reference and observe at the same
Counter instance, SwiftUI state keeps only one value for the
count property. This is because we’ve used
@ObservedObject on the
CounterView, which does not put anything into the state.
Thank you for reading, I hope it was helpful! 🙌
My Twitter in case you'd like to reach out.