Due to its declarative and reactive nature, SwiftUI works pretty well with this technique. Suppose we are building a screen that fetches a Github profile. We begin by describing the possible stages of this fetch process:
enum State: Equatable {
case `default`
case loading
case fetched(profile: GithubProfileViewModel)
case failure(error: HttpError)
}
In the view model, we declare a variable holding the state:
@MainActor
final class GithubProfileFetchViewModel: ObservableObject {
@Published
private(set) var state = State.default
func fetchProfile(using username: String) async {
// This method will update the state in the different stages
// of the fetch process. Notice the state is published.
}
// ...
}
Then in the View,
we use this state
when declaring our body
:
struct GithubProfileFetchView: View {
@StateObject
private var viewModel = GithubProfileFetchViewModel()
// ...
var body: some View {
VStack {
// ...
switch viewModel.state {
case .`default`:
DefaultProfileView()
case .fetched(let profileViewModel):
ScrollView {
GithubProfileView(viewModel: profileViewModel)
.padding()
}
case .loading:
LoadingIndicator()
case .failure(let error):
if error == .requestFailed(statusCode: 404) {
ErrorView.profileNotFound()
} else {
ErrorView.connectionError()
}
}
// ...
}
.task(id: shouldStartFetch) {
await fetchProfile()
}
}
// ...
}
The usage of switch
makes the code really readable. Whenever the state
changes, SwiftUI
presents a different view for us. Since SwiftUI
is declarative, we don’t need to setup bindings, and manually change what we display. This process is all done automatically for us.
Note that this technique works with other architectures too, and with other property wrappers (e.g @State
, @ObservableObject
, @EnvironmentObject
). You can check the full view model or view code.
You might also want to read this article by Apple.