DEV Community

Cover image for SwiftUI State Management Fundamentals
Mykola Harmash
Mykola Harmash

Posted on

SwiftUI State Management Fundamentals

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.


Prerequisites

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.


Application Pipeline

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:

Image description


Ephemeral Views

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.

Image description

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.


Triggering View Re-creation

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.

Image description

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.


@State

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.

Image description

@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.

Demo


Source code of the example

Notice that Counter view has init() with print() inside.

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.


View Re-creation vs. View Disappearance

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.


Source code of the example


shameless plug
I try to regularly post useful dev content on my Twitter, feel free to follow me there 😉


@State With Reference Types

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.

But @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.

Image description

If you try to run this code you’ll see that it does not work.


Source code of the example

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.


Source code of the example

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.


ObservableObject, @ObservedObject and @Published

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.

By using @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.

@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.

Image description
Source code of the example

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.


Source code of the example

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.


@StateObject

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.

Image description

Below, watch a demo with the Random button, where it uses the @StateObject on the counter property this time.


When to use @ObservedObject instead of @StateObject?

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.

Use @StateObject in the view which creates an object. Use @ObservedObject when the view receives objects from outside.

Consider this example:

Image description
Source code of the 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.

Top comments (0)