← Back to list

Manage loading state with Swift enum

SwiftUI and UIKit

Mohamed Aymen Landolsi · 2024-07-19 09:27 · 117 claps · 4.8 min read
#swift #swiftui #uikit #loading-state #enum
Open on Medium ↗
Wiki topics: 📱 · Mobile Development

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:

  1. We have 3 different properties:
  • isLoading drives the loading state display.
  • failed drives the success/error states display. true means success, false means failure. It can also represent a not loadingstate.
  • users drives the success state. It can also represent a not loadingstate.
  1. We handle each state according to the values of the view properties (isLoading, failed and users).
  2. Update the view properties according to the result of the fetchUsers method.

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