When Apple unveiled SwiftUI in 2019, it revolutionized the world of UI design, ushering in a new era of modern development platforms. For many developers, SwiftUI’s state management paradigm was the most significant change, yet it often flew under the radar. In this article, we’ll dive into managing state in SwiftUI, exploring why understanding this concept is vital for SwiftUI application state, and how it differs from legacy patterns. We’ll also share various state management techniques in SwiftUI to help you create more intuitive and powerful app interfaces.
Before we jump into it, I think it’s important that we briefly introduce SwiftUI to those who might need it. Feel free to skip to the next section if you don’t.
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.
SwiftUI is a framework designed to support this programming style. Additionally, it is particularly 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 called “body” of type View 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 lifecycle.
The key phrase here is “persistence throughout the view lifecycle,” as the OS must intrinsically preserve a state for it to be helpful.
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. Additionally, you can read more on how to use toggles in SwiftUI in this article here.
Why is state management critical?
Managing the state effectively 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 reason about 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 and 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.
In Summary
Managing state in SwiftUI can be a bit different than in other frameworks, but it is a powerful tool 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.
This post was originally written for and published by Waldo.com
Leave a Reply