Waldo sessions now support scripting! – Learn more
App Development

A Guide to Managing State in SwiftUI

Juan Reyes
Juan Reyes
A Guide to Managing State in SwiftUI
January 10, 2023
11
min read

When Apple introduced SwiftUI in 2019, it made waves with its bold new approach to UI design. For many, it was a welcome paradigm shift in a development platform that was falling behind.

There's no doubt that the approach to UI design that SwiftUI introduces is better and more intuitive. And having a modern tool for designing complex views is quite significant. However, I would argue that the most significant change Apple introduced flew under the radar: the new state management paradigm.

This article serves as an introductory guide to the concept of state management in SwiftUI. This article will go through what state is and why it's critical to understand it. Then, we will explore how it's different from the legacy patterns that handle the application state. Finally, we will explore the different approaches you can use to implement state management in SwiftUI.

Before we jump into it, let's briefly introduce SwiftUI to those who might be unfamiliar with it. Feel free to skip to the next section if you already have the hang of SwiftUI.

SwiftUI is a framework for building user interfaces on Apple platforms using a declarative programming style

What Is SwiftUI?

SwiftUI is a framework for building user interfaces on Apple platforms using a declarative programming style. In a declarative programming style, the developer specifies what the user interface should do rather than how it should do it. This can make it easier to reason about the behavior of the user interface and to build and maintain it.

SwiftUI is a framework designed to support this programming style. Additionally, it is well-suited for building user interfaces on Apple platforms like iOS, iPadOS, macOS, and watchOS.

In Apple's official documentation, you can find that "SwiftUI provides views, controls, and layout structures for declaring your app's user interface. The framework provides event handlers for delivering taps, gestures, and other types of input to your app, and tools to manage the flow of data from your app's models down to the views and controls that users see and interact with."

A typical fresh SwiftUI project starts with two files in it: a ContentView.swift file and an <APP_NAME>App.swift file, where APP_NAME is the name you used for the project.

In SwiftUI, View Classes define views and have a standard structure. A View struct specifies the structure and behavior of the view, while a PreviewView struct allows the emulator to display a live preview of your work.

There is also a variable type of View called body that defines the content of the ContentView. Any changes to this variable will result in corresponding changes to the appearance of the current view.

All new view classes typically contain a simple TextView element with the text "Hello World!"

If you're interested in learning more about the inner workings of SwiftUI, I recommend checking out these other articles.

What Is State in SwiftUI?

As mentioned before, one of the key concepts in SwiftUI is managing state, which refers to the data that drives the behavior of a user interface.

In app development, state refers to the data that drives the behavior and rendering of a user interface. This can include simple data like a toggle switch's on/off state and more complex data like a list of items in a table view.

In SwiftUI, state is stored in a view's struct and updated using mutating methods. For example, consider a simple toggle that changes the text when it's turned on:


  struct ToggleView: View {
    @State var isOn: Bool = false
    
    var body: some View {
        VStack {
            Toggle(isOn: $isOn) {
                if isOn {
                    Text("Toggle On")
                } else {
                    Text("Toggle Off")
                }
            }
        }
    }
}

In this example, the isOn state variable determines what text to display on the label. Notice that I marked the isOn variable with the @State property wrapper, which indicates that it is a state variable that the view can modify. This marker allows the Toggle view to update the value of isOn when the user toggles it. SwiftUI can then persist this value throughout the view life cycle.

The key phrase here is "persistence throughout the view life cycle," as the OS must intrinsically preserve a state for it to be helpful.

SwiftUI State Bindings

One thing to note is that you should only modify state variables from within the view's body. If you need to update the state from an external source, such as a network request, you can use a Binding to pass the state to the view.

Here's an example of a simple use of a Binding to request and enforce the use of an external state variable:


  struct ToggleView: View {
    @Binding var isOn: Bool
    
    var body: some View {
        VStack {
            Toggle(isOn: $isOn) {
                if isOn {
                    Text("Toggle On")
                } else {
                    Text("Toggle Off")
                }
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    @State static var isOn: Bool = false
    
    static var previews: some View {
        ContentView(isOn: $isOn)
    }
}

In this case, the isOn state is passed to the ToggleView as a Binding. This allows the struct to update the state from an external source.

If you feel like you don't follow, don't worry. I will explain further later.

Why Is State Management Critical?

Effectively managing state is critical for building apps that are easy to understand, maintain, and extend. When the developer does not appropriately manage the state, it can become difficult to understand the behavior of an app, leading to bugs and a poor user experience.

Implementing State Management in SwiftUI

There are a few different approaches to managing state in SwiftUI. Which one you choose will depend on the needs of your app.

Here are a few standard options:

@State and @Binding

As you might have seen already, the most common way to implement state management in SwiftUI is to use the @State and @Binding property wrappers. These property wrappers allow you to store and manage state directly on a view, and to pass state between views using bindings.

To use @State, you must declare a property on your view and mark it with the @State property wrapper. Any changes to this property will cause the view to be automatically refreshed.

@Binding works similarly, allowing you to pass the state between views. To use @Binding, you declare a property on a view, mark it with the @Binding property wrapper, and then pass in a reference to the state you want to bind to. Any changes to the state will be automatically reflected in the view that is bound to it.

Here's an example of a @State and @Binding implementation.


  struct ContentView: View {
    @State private var message = "Write a message"
    
    var body: some View {
        VStack {
            MessageFormView(message: $message)
        }.padding()
    }
}

struct MessageFormView: View {
    // The message to be sent
    @Binding var message: String
    @State private var showingAlert = false

    var body: some View {
        VStack {
            // A text field bound to the message state
            TextField("Enter message", text: $message)
                .textFieldStyle(RoundedBorderTextFieldStyle())

            // A button that sends the message when tapped
            Button("Show Alert") {
                showingAlert = true
            }
            .alert(message,
                   isPresented: $showingAlert) {
                Button("OK", role: .cancel) { }
            }
        }
    }
}

In this example, the MessageFormView has an @Binding property called message that is a String. The body of the view consists of a text field and a button. The text field is bound to the message state using the dollar sign syntax ($message). This creates a two-way binding between the text field and the message state. Any changes to the text field will be automatically reflected in the message state coming from the ContentView struct. Furthermore, any changes to the message state will automatically be reflected in the text field.

When the button is tapped, it triggers the alert, which displays an alert with the text. In your app, however, you could add a sendMessage() function that could send the message to a server using an API or some other mechanism.

ViewModel

Another approach to state management in SwiftUI is to use a ViewModel. A ViewModel is an object that acts as a bridge between a view and its data. It exposes the data that the view needs in a way that is easy for the view to consume, and it can also handle any logic or transformations that need to be applied to the data before it is displayed.

To use a ViewModel in a SwiftUI view, you can create a property on the view that is an instance of the ViewModel, and then bind the view's controls to properties on the ViewModel. This way, when the user interacts with the controls, the ViewModel's properties will be updated, and the view will be automatically refreshed to reflect the changes.

ObservableObject and @ObservedObject

A third option for managing state in SwiftUI is to use the ObservableObject and @ObservedObject property wrappers.

ObservableObject is a protocol you can adopt to create objects that can be observed for changes. When an object that conforms to ObservableObject changes, any views that are bound to it will be automatically refreshed.

To use ObservableObject in a SwiftUI view, you can create a property on the view that is marked with the @ObservedObject property wrapper. You can then pass in an instance of the ObservableObject. Any changes to the ObservableObject will be automatically reflected in the view.

You can find the complete code here.

By using state variables and bindings, you can create views that respond to changes in data and user input in a declarative and intuitive way.

In Summary

Managing state in SwiftUI can be a bit different than in other frameworks, but it is a powerful technique for building dynamic and interactive user interfaces. By using state variables and bindings, you can create views that respond to changes in data and user input in a declarative and intuitive way.

However, when not used properly, it's very easy to introduce bugs that are tricky to address. That is why having a solid testing workflow in your app is essential.

I recommend checking out Waldo's comprehensive toolset for UI testing if you want to avoid the complexities of developing a testing workflow. This toolset requires no coding and is easy to use, even for those who are not developers.

Automated E2E tests for your mobile app

Waldo provides the best-in-class runtime for all your mobile testing needs.
Get true E2E testing in minutes, not months.

Reproduce, capture, and share bugs fast!

Waldo Sessions helps mobile teams reproduce bugs, while compiling detailed bug reports in real time.