Navigate back to the homepage

State Management in SwiftUI

Arvind Ravi
May 5th, 2021 · 3 min read

Apps: What do they all need?

Fundamentally, all apps have three types of requirements:

  • Keep track of and mutate state.
  • Respond to user actions or other types of events (say notifications, asynchronous callback, etc) by mutating state.
  • Trigger functions using dependencies and do the above.

Broadly speaking most apps deal with two or more of the above requirements. SwiftUI offers a way to deal with these requirements in a consistent fashion and along with it, introduces a declerative API to build our UI.

The Case for SwiftUI

SwiftUI is a fantastic first party solution by  to deal with these requirements without having to introduce any external dependencies and while allowing to build and iterate UI very quickly.

SwiftUI: What it is

SwiftUI does two things:

  • Lets us build UI declaratively
  • Lets us manage state and dependencies

To keep things within the scope of this post, let’s ignore the UI part and focus on how it let’s us manage state and dependencies. It introduces a few different property wrappers to aid state and dependency management, some of which are:

  • @State

This lets us define mutable properties local to a view. Any change to these properties will cause the view to be re-rendered.

Example:

1struct ContentView: View {
2 @State
3 var backgroundColor: Color = .green
4
5 var body: some View {
6 Text("Hello, world!")
7 .padding()
8 .background(backgroundColor)
9 }

In this example backgroundColor is a state that the view manages and anytime it changes, the view gets re-rendered.

  • @StateObject

This lets us create properties of reference type data to be held within our views, using this ensures that the property stays alive in that view until we need it for the lifecycle of the view.

Example:

Say you have a view model that you would like to use to drive a view:

1class ViewModel: ObservableObject {
2 let title: String
3
4 init(title: String) {
5 self.title = title
6 }
7}

The view:

1struct ContentView: View {
2 @State
3 var backgroundColor: Color = .green
4
5 @StateObject
6 var viewModel = ViewModel(title: "Hello")
7
8 var body: some View {
9 Text(viewModel.title)
10 .padding()
11 .background(backgroundColor)
12 }
13}

In this case using @StateObject ensures that the property (which is of a reference type) can be held onto through the view’s lifetime before it gets released.

  • @Environment

This is a way to read information from the system - trait collection, color scheme, etc.

Example:

1@Environment(\.horizontalSizeClass) var horizontalSizeClass
  • @EnvironmentObject

Similar to @StateObject, this allows us to hold onto a property that conforms to ObservableObject within the view, with one difference being — unlike the state object which expects the object to be provided by the view during initialisation, EnvironmentObject expects the SwiftUI environment to provide it.

Example: Say we have a view model like:

1class Order: ObservableObject {
2 @Published var items = [String]()
3}

The View:

1struct ContentView: View {
2 @EnvironmentObject var order: Order
3
4 var body: some View {
5 // view code
6 }
7}

The view now has a dependency to order which would be provided by the environment using the .envionment() modifier. This is a good way to handle dependencies in the app like so:

1ContentView()
2 .environment(Order())

SwiftUI: What it isn’t

While all of this is great and you could build fully functioning apps with what SwiftUI provides out of the box, we would quickly notice a pattern where state is mutated in multiple points across the app. As the app grows with multiple screens and dependencies - we would have been mutating state in multiple places, handling events and dependencies within a soup of UI code.

Here’s a simple example:

1struct ContentView: View {
2 @State var todos: [Todo] = []
3
4 var body: some View {
5 NavigationView {
6 List {
7 ForEach(Array(zip(todos.indices.sorted(), todos)), id: \.0) { index, todo in
8 HStack {
9 Button(action: {
10 todos[index].isComplete.toggle()
11 }) {
12 Image(systemName: todo.isComplete ? "square.fill" : "square")
13 }
14 .buttonStyle(PlainButtonStyle())
15 TextField("Untitled todo",
16 text: $todos[index].body)
17 .foregroundColor(todos[index].isComplete ? .gray : nil)
18 }
19 }
20 }.navigationTitle("Todos")
21 }
22 }
23}

Scattered State Mutation

In this example we see that the state is being mutated in a few different places and an action being performed, which inturn mutates state. Having this happen across many different views we would quickly be in position where our business logic is spread out over a lot of views and abstractions which would have state being mutated in many different places, both local state and global.

Bindings, while they’re fantastic in certain cases, like to present an alert for instance:

.alert(self.$alert) { }

While this is a great API, alert gets set to nil once the alert is dismissed which is a state mutation that happens under the hood, which we might need to know about.

Side Effects

Side Effects are things that have an effect on the state of the app during it’s lifetime. Think async API calls, notifications, etc. This has traditionally been handled with closures, SwiftUI also adopts a very similar approach of using them within closures, for example:

1func fetchImage(id: Int) {
2 apiClient.fetch(imageID: id) { image in
3 self.image = image
4 }
5}

In this instance, the effect or the async call fetch is being fired off and we’re hoping to get a response back the way we expect, while this is how effects have always worked, the effect by itself cannot be controlled since it’s isolated with closures and we expect to receive a response at this point. Once it’s been fired off, we cannot cancel it either.

Sure we can ensure that it works as expected by writing some tests - but wouldn’t it be better if we had a data type to control the uncertainity of effects? I tend to like work with guaranteed outcome whenever possible, there’s definitely different types of effects that could have different degrees of unpredictability.

-

While this isn’t a criticism of SwiftUI itself, I feel like dealing with software where state gets mutated all over the place could be harder to maintain than with centralised state manipulation and being able to guarantee the behaviour of effects in the app using concrete types would definitely be nicer when it comes to working with asychronous effects.

Hmm, if only there was a way to do that.

More articles from Swiftla

Mobility Metrics: Calculating Walking Speed

Do you know you can now calculate mobility metrics like walking speed with iOS 14? Learn how.

March 7th, 2021 · 2 min read

Thinking in SwiftUI

Learn how to think in SwiftUI.

October 14th, 2020 · 2 min read
© 2017–2021 Swiftla
Link to $https://twitter.com/arvindravi_Link to $https://github.com/arvindraviLink to $https://www.linkedin.com/in/arvindravizxc/