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
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:
swiftenum 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
swiftclass 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
swiftstruct 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 theImageLoaderViewModel
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.
- Loading: Displays a
- 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.