Using Enum to Handle States in SwiftUI with MVVM architecture

No imageSilviu Vranau
9 September, 2024

When developing SwiftUI applications, it’s common to deal with different states, such as loading data, handling errors, and displaying results.

In this article, we will explore how to use an enum to handle loading states for an image downloaded from a remote source using the MVVM (Model-View-ViewModel) architecture and Swift's async/await.

Output

No Image

Problem Overview

We want to display an image in our SwiftUI app that is fetched from a remote URL. While fetching the image, we need to account for the following states:

  • Loading: The app is currently fetching the image.
  • Failed: The app encountered an error while fetching the image.
  • Success: The image has been successfully loaded.

Enum for Managing States

We can define an enum to represent these states in a clear and type-safe way:

swift
enum ImageLoadingState { case loading case success(image: UIImage) case failed(error: Error) }

This enum helps ensure that the UI reacts appropriately to each state: showing a loading spinner when data is being fetched, an error message when it fails, or the image when the download succeeds.

MVVM Architecture

In the MVVM architecture:

  • Model: Holds the data (in our case, the image).
  • View: Displays the UI and reacts to state changes.
  • ViewModel: Handles the business logic, including fetching the image and updating the ImageLoadingState.

Ideally, the ViewModel would have a service injected into it to handle the networking logic, separating concerns and promoting better testability and code organization. However, for the sake of this example, we will include the networking logic directly within the ViewModel.

View model example

swift
class ImageLoaderViewModel: ObservableObject { @Published var state: ImageLoadingState = .loading func fetchImage(from urlString: String = "https://picsum.photos/200") async { guard let url = URL(string: urlString) else { self.state = .failed(error: URLError(.badURL)) return } do { let (data, _) = try await URLSession.shared.data(from: url) if let image = UIImage(data: data) { self.state = .success(image: image) } else { throw URLError(.cannotDecodeContentData) } } catch { self.state = .failed(error: error) } } }

This ViewModel declares a @Published state that updates based on the result of fetching a remote image. It includes a function that uses URLSession to perform the fetch. If the image is successfully retrieved, the state is set to success; if it fails, the state is set to failed. By default, the state is initialized as loading.

Simple View example

swift
struct RemoteImageView: View { @StateObject private var viewModel = ImageLoaderViewModel() var body: some View { Group { switch viewModel.state { case .loading: ProgressView("Loading Image...") .progressViewStyle(CircularProgressViewStyle()) case .success(let image): Image(uiImage: image) .resizable() .scaledToFit() .frame(width: 300, height: 300) .clipShape(RoundedRectangle(cornerRadius: 10)) case .failed(let error): Text("Failed to load image \(error)") .foregroundColor(.red) } } .task { await viewModel.fetchImage() } } }
  • The @StateObject ensures that the ImageLoaderViewModel is instantiated and its state changes are reflected in the view.
  • The Group contains a switch statement that determines what UI to display based on the current state:
    • Loading: Displays a ProgressView.
    • Success: Displays the fetched image using Image(uiImage:).
    • Failed: Shows an error message.
  • The .task modifier runs the fetchImage function asynchronously when the view appears. It is also used to take advantage of automatic task cancellation.

That is all. This approach provides a clean and organized way to handle different states in a SwiftUI MVVM architecture.


Thanks for stopping by! 🪴