UI State Management in SwiftUI
Exploring Simple Strategies for UI State Management in SwiftUI
Introduction
In SwiftUI, when we start creating a new View
, one of the first things we thought is the UI states, like what it should look like when that one View of the app is still fetching data, encounters an error, when data is empty, and when there is data.
We usually start simple, something like this:
@State private isLoading: Bool = false
var body: some View {
ZStack {
if isLoading {
ProgressView()
} else {
Text("Hello World!")
}
}
}
Then at some point, it gets messy though somehow manageable, like this:
@State private isLoading: Bool = false
@State private hasError: Bool = false
@State private hasData: Bool = false
var body: some View {
ZStack {
if isLoading {
ProgressView()
} else if hasError {
Text("Encountered Error")
} else if hasData {
Text("Data is loaded here")
} else if !hasData {
Text("There is no data....")
}
}
}
Notice that every time we add a state, we create a new @State
variable, surely things will get crowded.
Enum Driven
Years ago, I came across an interesting approach called 'enum-driven', which is a good strategy for handling distinct states or values. This technique works well when these states are exclusive and don't overlap with one another.
So in this context, let us assume that states can never be both hasData
and hasError.
enum ViewState {
case loading
case populated
case empty
case error
}
With Enum, we can use switch-case
statements.
switch viewModel.state {
case .populated:
Text("Populated")
case .empty:
Text("Empty")
case .loading:
ProgressView()
case .error:
Text("Error")
}
Manage within its View
In your View
, create a @StateObject
property with the variable name state
.
For the sake of this example we shall make a simple View struct, and using ZStack
.
@State private var state: ViewState = .empty
var body: some View {
ZStack {
switch state {
case .populated:
Text("Populated")
case .empty:
Text("Empty")
case .loading:
ProgressView()
case .error:
Text("Error")
}
}
}
if you want to simulate a loading state you may attach a ViewModifier
on the ZStack
.
.onAppear {
state = .loading
DispatchQueue.main.asyncAfter(
deadline: .now() + .seconds(Int(3.0))) {
state = .populated
}
}
Manage with ViewModels
We have to conform the ViewModel to ObservableObject
so the View
can observe the changes.
@Published
allows us to create observable objects that automatically publish or announce when changes occur.
Apple docs states
Publishing a property with the
Published
attribute creates a publisher of this type.When the property changes, publishing occurs in the property’s
willSet
block, meaning subscribers receive the new value before it’s actually set on the property.
class CustomViewVMViewModel: ObservableObject {
@Published var state: ViewState = .empty
}
switch viewModel.state {
case .populated:
Text("Populated")
case .empty:
Text("Empty")
case .loading:
ProgressView()
case .error:
Text("Error")
}
Manage by its parent View
Create the parent View.
struct CustomContainerMainView: View {
@State private var state: ViewState = .empty
var body: some View {
CustomChildView(state: $state)
}
}
then the Child View.
struct CustomChildView: View {
@Binding var state: ViewState
var body: some View {
ZStack {
VStack {
switch state {
case .populated:
Text("Populated")
case .empty:
Text("Empty")
case .loading:
ProgressView()
case .error:
Text("Error")
}
Button {
state = .loading
} label: {
Text("Start Loading")
}
Button {
state = .error
} label: {
Text("Show Error")
}
Button {
state = .populated
} label: {
Text("Show Populuted")
}
}
}
}
}
Notice that we use @Binding
instead of let
, so we are giving the child view the power to change the value of the state that was owned by the Parent View.
Using between @Binding var
vs let
will depend on you.
To illustrate the loading state, update your parent view by adding a Button
to start loading.
VStack {
CustomChildView(state: $state)
Button {
state = .loading
} label: {
Text("Parent Button - Start Loading")
}
.buttonStyle(.borderedProminent)
}
Final Thoughts
When implementing this we have to keep in mind and ask these questions.
"Who should be responsible for managing the state of this particular View?"
"Are we putting too much responsibility on this View, or ViewModel, or ParentView?"
"Do we have to always follow this pattern?"
"How many States or Cases should this particular view have?"
"Are we making this too complicated or too simple?"
"Does this scale well, flexible, and maintainable?"
The implementation will depend on the use case, scenario, and needs.
Let me know if you have similar implementations, and have other scenarios. From here, I'll be exploring more, experimenting more, and trying different complex scenarios.
Thanks for reading! 📖
You can check the sample project here