Manage loading state with Swift enum
SwiftUI and UIKit
Manage loading state with Swift enum
SwiftUI and UIKit

When building an iOS application, one should take good care of screens which are driven by loading states. This can be ensured by taking good care of the source code which handles this type of state. In this article we are going to explore two different ways for managing view state:
- different properties which altogether represent the view state.
- a single property backed by an enum for representing the view state.
Then we are going to enhance the second approach and make more generic.
Note:in this article we are going to explore both approaches for SwiftUI views. You can explore the UIKit part by taking a look at the full source code which you can find here.
Managing state with multiple properties
Without any transition, let’s begin by an example of a screen which displays a list of users.
*Note:* the users list should be fetched from a HTTP API, but for the sake of the demo we will simply fake the API call.
// MARK: - UserListView
struct UserListView: View {
// 1.
@State private var isLoading = false
@State private var failed = false
@State private var users = [User]()
var body: some View {
// 2.
Group {
if self.isLoading {
// display progress
} else if self.failed {
// display failure
} else {
// display users
}
}
.onAppear { self.fetchUsers() }
}
}
private extension UserListView {
private func fetchUsers() {
// 3.
self.isLoading = true
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
if Bool.random() {
// success
// 3.1
self.users = [.init(name: "User 0"), .init(name: "User 1")]
// 3.2
self.failed = false
} else {
// failure
// 3.3
self.failed = true
}
// 3.4
self.isLoading = false
}
}
}
Let’s did through UserListView:
- We have 3 different properties:
isLoadingdrives the loading state display.faileddrives the success/error states display.truemeans success,falsemeans failure. It can also represent anot loadingstate.usersdrives the success state. It can also represent anot loadingstate.
- We handle each state according to the values of the view properties (
isLoading,failedandusers). - Update the view properties according to the result of the
fetchUsersmethod.
This it might look good, but once you begin testing your code you may find out in some cases that the view does not display the state correctly. This is due to the way we wrote the is else statements in listing 1.
This can be fixed by rewriting the if else statement in listing 1 . But bare in mind that you have to pay attention to the way you write this if else statement in every single view that handles a loading state.
How can we improve this behavior ? Let’s find this out by exploring the second approach.
Manage state with Swift enum
Instead of having multiple properties representing the view’s state, we can have only one property which is backed by an enum.
enum UserListLoadingState {
// Nothing is going on.
case idle
// Loading state.
case loading
// Success state by providing the list of users.
case success([User])
// Failure state.
case failure
}
Now let’s refactor UserListView by using the enum above:
// MARK: - UserListView
struct UserListView: View {
// 1.
@State private var loadingState = UserListLoadingState.idle
var body: some View {
// 2.
Group {
switch self.loadingState {
case .loading:
// Dipslay progress
break
case let .success(users):
// Display users
break
case .failure:
// Display failure
break
case .idle:
// Display view's initial state. Could be en EmptyView()
break
}
}
.onAppear { self.fetchUsers() }
}
}
private extension UserListView {
private func fetchUsers() {
// 3.
self.loadingState = .loading
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
if Bool.random() {
// success
self.loadingState = .success([.init(name: "User 0"), .init(name: "User 1")])
} else {
// failure
self.loadingState = .failure
}
}
}
}
Now it looks better. We only have to update a single property to refresh the view’s state. Te view loading state is managed by a single source of truth which is the loadingState property. This is easier than updating 3 different properties and it’s even less error prone.
This seems good. But what if we had other views which have different loading states ? That means each view will have to declare its own LoadingState enum type.
Let’s take this example:
import SwiftUI
import Foundation
enum UserDetailLoadingState {
case idle
case loading
case success(UserDetail)
case failure
}
// MARK: - UserDetailLoadingState
struct UserDetail {
let age: Int
let height: Int
}
// MARK: - UserDetailView
struct UserDetailView: View {
@State private var loadingState = UserDetailLoadingState.idle
var body: some View {
Group {
switch self.loadingState {
case .loading:
// Dipslay progress
break
case let .success(userDetail):
// Display userDetail
break
case .failure:
// Display failure
break
case .idle:
// Display view's initial state. Could be en EmptyView()
break
}
}
.onAppear { self.fetchUsers() }
}
}
We declared an enum called UserDetailLoadingState which looks the same as UserListLoadingState with one difference which is the type of the data structure associated to the success case.
It looks like we are repeating ourselves here, so why don’t refactor our code and declare a generic reusable enum?
// MARK: - LoadingState
enum LoadingState<T> {
case idle
case loading
case success(T)
case failure
}
Now we can go back to both both views and refactor them by using the generic LoadingState enum:
// MARK: - UserListView
struct UserListView: View {
@State private var loadingState = LoadingState<[User]>.idle
var body: some View {
// ...
}
}
// MARK: - UserDetailView
struct UserDetailView: View {
@State private var loadingState = LoadingState<[UserDetail]>.idle
var body: some View {
// ...
}
}
Now it’s even better, but wait…there is more.
In some cases, views do not want to handle failure state because in some cases it simply does not make sense.
In that case, we can refactor our generic LoadingState enum to support this kind of use case:
enum LoadingState<T> {
case idle
case loading
case success(T)
case failure
}
// Becomes
enum LoadingState<T> {
case idle
case loading
case loaded(T)
}
In caseUserListView‘s state can fail then UserListView‘s loadingState property type becomes LoadingState<Result<[User], Error>> .
struct UserListView: View {
// 1
@State private var loadingState = LoadingState<Result<[User], Error>>.idle
var body: some View {
Group {
switch self.loadingState {
case .loading:
// Dipslay progress.
break
case let .loaded(.success(users)):
// Display users.
break
case let .loaded(.failure(error)):
// Display failure.
break
case .idle:
// Display view's initial state. Could be en EmptyView()
break
}
}
.onAppear { self.fetchUsers() }
}
}
And in case it wouldn’t make sense to handle failure then UserListView‘s loadingState property type becomes LoadingState<[User]> (no error handling).
struct UserListView: View {
// 1
@State private var loadingState = LoadingState<[User]>.idle
var body: some View {
Group {
switch self.loadingState {
case .loading:
// Dipslay progress.
break
case let .loaded(users):
// Display users.
break
case .idle:
// Display view's initial state. Could be en EmptyView()
break
}
}
.onAppear { self.fetchUsers() }
}
}
That way our generic LoadingState enum can be used both states with and without failure case.
Conclusion
By following this approach you can avoid duplicating code and introducing bugs due to bad state management when writing your code.
Thanks for your time and patience.
You can find the final version of the source code here.
Any feedback is appreciated !
메타데이터
- post_id
- 82677e4dbb13
- slug
- manage-loading-state-with-swift-enum-82677e4dbb13
- url
- https://medium.com/@rokridi/manage-loading-state-with-swift-enum-82677e4dbb13
- canonical_url
- https://medium.com/@rokridi/manage-loading-state-with-swift-enum-82677e4dbb13
- author_url
- https://medium.com/@rokridi
- status
- ok
- fetched_at
- 2026-08-25 01:30:14