Beginners Guide to SwiftUI State Management
Delve into the intricacies of state management in SwiftUI, exploring different strategies you can employ to utilize Apple’s powerful state management APIs.
Join the DZone community and get the full member experience.
Join For FreeState management is a fundamental concept in app development that involves tracking and updating data that influences the user interface. In SwiftUI, this is particularly crucial due to its declarative nature. To effectively leverage SwiftUI’s capabilities, it’s essential to grasp the various approaches to state management and why they are necessary.
This article will delve into the intricacies of state management in SwiftUI. We’ll explore different strategies that programmers can employ to utilize Apple’s powerful state management APIs. Before diving into SwiftUI-specific techniques, let’s examine the broader context of UI programming and understand why state management is indispensable.
Imperative Programming: UIKit, AppKit, and WatchKit
Before SwiftUI, iOS and tvOS developers relied on UIKit, macOS developers used AppKit, and watchOS developers worked with WatchKit. These frameworks, all based on the imperative programming paradigm, offered event-driven UI development.
Key Improvements
- Clarity: The sentence explicitly states which frameworks were used for each platform.
- Context: It mentions that these frameworks were based on the imperative programming paradigm.
- Conciseness: The sentence is streamlined while maintaining essential information.
Declarative Programming: SwiftUI
SwiftUI leverages the declarative programming approach, allowing developers to define the desired outcome, and the framework handles the underlying implementation details.
In the example below, if we instruct SwiftUI to display a welcome message when logged in and a login button when logged out, it will automatically update the view to reflect changes in authentication state.
A major advantage of learning and using SwiftUI is that it allows for cross-platform development for the different operating systems within the Apple ecosystem — no more having to learn four different frameworks if you want to build an app that has components on Apple Watch, Apple TV, MacBook Pro, and finally your iPhone.
It is important to know that SwiftUI does not replace UIKit. Instead, it is built on top of providing and an additional layer of abstractions for us.
State in SwiftUI
State refers to the current data that a view or a group of views relies on to present the UI. In SwiftUI, when the state changes, the UI automatically reflects these changes. This is because SwiftUI is declarative, meaning you describe what the UI should look like based on the current data, and SwiftUI handles updating the UI when the data changes.
There are several ways to handle state in SwiftUI:
@State
, used to manage readonly local state@Binding
, used to manage read/write state@ObservableObject
, used to manage external data models@StateObject
, used to manage object lifecycle within a view
Let’s explore these types in detail one by one.
First, let's create an example iOS project that we will change to explore different state types.
By default, you will get the following view:
@State
@State
is used to manage local state within a single view. It is the simplest and most frequently used state management tool in SwiftUI. When you mark a property with @State
, SwiftUI takes ownership of that data and ensures the view re-renders when the state changes.
Change the ContentView
as follows:
Let’s explore the code:
- We declared a private count variable initializing it to
0
. - We annotated the variable with
@State
, telling SwiftUI to observe the changes to this variable and update the view accordingly. - In the body, we created a
Text
label that displays the count. - Below the label, we added a button, and in the action, we incremented the variable.
You don’t have to run the project to see it working: you can tap the increment button in the preview and it will work.
As button is pressed, count
is incremented and Text
label is updated with the current value.
So, @State
annotation is declaring the count variable as an state. Whenever count is updated, SwiftUI checks if any UI element is using it and updates the view accordingly.
As an experiment, try to remove @State
from the count
variable. You will encounter an error.
The error says: “Left side of mutating operator isn’t mutable: ‘self’ is immutable”.
What does this mean? count
is variable, so it should have no problems updating. But if you look closely, the error says "self is immutable," which means that our SwiftUI’s view is immutable and cannot be changed like this. @State
makes our SwiftUI views mutable.
Two-Way Binding via @State
In the previous example, our Text
element was reading the count value, but the increment was not done via some UI element. Instead, it was done via our own implementation we provided to button action.
There are SwiftUI elements that directly change the data, e.g., Toggle
or TextField
. In such a case, we can provide our @State
variable following with $
. This is called binding.
Change the view to add two HStack
s: one with Text
and TextField
, the other with Text
.
In the code above,
- We added a new state variable
name
of type String. - We created two
HStacks
, one withTextField
and another one displaying entered text. - The display
Text
uses the same format to displayname
as we used for displaying counter. - Notice how
TextField
is taking$name
. This is called binding. - If you type a name in the
TextField
, the updated name will be reflected instantly in theText
.
If you Option+Click the TextField
element, a declaration popup comes up with TextField
details. Notice the type of text Binding<String>
. We will explore the binding further in the next section.
@Binding
In the previous example, we used TextField
’s text binding to update the state of our view ContentView
. So, if we look closely, what is happening here is that the child element (i.e., TextField
) is updating the state of the parent (ContentView
). So, @Binding
is used mainly in a scenario where a child element wants to update the state of its parent element.
@Binding
creates a connection with the parent’s state without owning the state itself.
As an example, let’s create parent/child views and update the state of parent from child view.
Let’s dissect the code:
- We created our custom view named
ToggleSwitch
. ToggleSwitch
has anisOn
boolean variable marked with@Binding
.@Binding
tells the SwiftUI that theisOn
variable is an external state that can be updated by the view.- In the
ParentView
, we declared theisOn
boolean variable just like we did before. - We passed the
isOn
variable inToggleSwitch
preceded with$
, informing that it is a two-way binding; i.e., state can be changed by the child view.
@ObservableObject
While @State
is great for simple and local state, more complex data models often need to be shared across multiple views. In this case, SwiftUI introduces the ObservableObject
protocol combined with the @ObservedObject
property wrapper. This allows you to create external data models that viewers can observe for changes.
To use the observable object, we need to know the following:
- A class that conforms to the
ObservableObject
protocol can hold data that is shared between views. - It uses the
@Published
property wrapper to notify views when the data changes.
Lets create a model class named ProfileModel
that holds the data and a corresponding view class named ProfileView
.
Let’s go through the details:
ProfileModel
is a simple class holding profile data.- The class conforms to the
ObservableObject
protocol. - All the properties are marked with
@Published
. ProfileView
class contains a variable ofProfileModel
class marked with@ObservableObject
.@Published
properties ofProfileModel
are used as state and binding.- Any change to properties via UI elements is reflected in the detail section as well.
We kept editing and displaying data on the same screen for simplicity’s sake, but in real world applications, these can be two different screens and the ProfileModel
can be owned by a central (possibly a singleton) object external to the view.
@StateObject
Introduced in SwiftUI 2.0, @StateObject
is used to create and manage the lifecycle of an ObservableObject
within a view. It ensures that the object is only created once and is not re-created unnecessarily during re-renders.
The main difference between @StateObject
and @ObservedObject
is that @StateObject
is responsible for creating the object, while @ObservedObject
is used when the object is created elsewhere (like in a parent view).
Implementation-wise, it’s quite similar to @ObservableObject
.
Use @StateObject
when a view is responsible for creating and owning the ObservableObject
.
It ensures the object is only created once, even when the view is re-rendered.
@Environment
For data that should be shared with many views in your app, SwiftUI gives us the @EnvironmentObject
property wrapper. This lets us share model data anywhere it’s needed, while also ensuring that our views automatically stay updated when that data changes.
Just like @ObservedObject
, you never assign a value to an @EnvironmentObject
property. Instead, it should be passed in from elsewhere, and ultimately you’re probably going to want to use @StateObject
to create it somewhere.
However, unlike @ObservedObject
, we don’t pass our objects into other views by hand. Instead, we send the data into a modifier called environmentObject()
, which makes the object available in SwiftUI’s environment for that view plus any others inside it.
- Note: Environment objects must be supplied by an ancestor view — if SwiftUI can’t find an environment object of the correct type you’ll get a crash. This applies for previews too, so be careful.
As an example of environment object:
In the above code:
- We created an
ObservableObject
class namedScoreBoard
that holds our score. ScoreView
class displays our score.- Note how
ScoreView
is extracting theScoreBoard
variable from the environment automatically by just declaring the property wrapper@EnvironmentObject
. ScoringView
is the main view that createsScoreBoard
object, update its values, and also passes it to the SwiftUI environment so that it is available to other views.
Best Practices and Common Pitfalls
- Use the right tool:
@State
for simple, view-local state@Binding
for parent-child communication@ObservedObject
or@StateObject
for managing more complex, shared data models@EnvironmentObject
for global or shared app-wide state
- Avoid overusing
@StateObject
or@ObservedObject
: Only recreate objects when necessary to avoid performance issues or unnecessary re-renders. - Avoid unnecessary UI re-renders: When possible, isolate state updates to the smallest view hierarchy possible.
Conclusion
In SwiftUI, state management is a fundamental aspect of building dynamic and interactive apps. By using the correct tools (@State
, @Binding
, @ObservedObject
, @StateObject
, and @EnvironmentObject
), you can efficiently manage your app’s state, ensuring a smooth and scalable user experience.
Published at DZone with permission of Sridhar Rao Muthineni. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments