Async/await and SwiftUI
Tags: swiftui, async, ios, modular
Published: 2024-06-20 12:28:34

Using Swift's async/await with SwiftUI can greatly simplify handling asynchronous tasks, such as fetching data from a network. Here's a basic example that includes a view, view model, use-case, repository, and service layer to illustrate how these components interact.

See [my Github project for the tested source-code](https://github.com/ishouldhaveknown/ios-swiftui-asyncawait).

### 1. Service Layer

First, let's define a service layer responsible for fetching data. This could be a simple API service. `APIService` conforms to `APIServiceProtocol` and simulates fetching data from an API.

```swift
import Foundation

protocol APIServiceProtocol {
    func fetchData() async throws -> String
}

class APIService: APIServiceProtocol {
    func fetchData() async throws -> String {
        // Simulate network delay
        try await Task.sleep(nanoseconds: 1_000_000_000)
        return "Data from API"
    }
}
```

### 2. Repository Layer

The repository layer abstracts the data source (service layer) from the rest of the application. `Repository` conforms to `RepositoryProtocol` and uses the `APIService` to get data.

```swift
import Foundation

protocol RepositoryProtocol {
    func getData() async throws -> String
}

class Repository: RepositoryProtocol {
    private let apiService: APIServiceProtocol
    
    init(apiService: APIServiceProtocol = APIService()) {
        self.apiService = apiService
    }
    
    func getData() async throws -> String {
        return try await apiService.fetchData()
    }
}
```

### 3. Use-Case Layer

The use-case layer contains the business logic. In this case, it fetches data using the repository. `FetchDataUseCase` conforms to `FetchDataUseCaseProtocol` and uses the repository to fetch data.

```swift
import Foundation

protocol FetchDataUseCaseProtocol {
    func execute() async throws -> String
}

class FetchDataUseCase: FetchDataUseCaseProtocol {
    private let repository: RepositoryProtocol
    
    init(repository: RepositoryProtocol = Repository()) {
        self.repository = repository
    }
    
    func execute() async throws -> String {
        return try await repository.getData()
    }
}
```

### 4. ViewModel

The view model interacts with the use-case layer and provides data to the view. `DataViewModel` is an `ObservableObject` that handles data fetching asynchronously using the use-case. It manages loading state, data, and potential error messages. Using `async/await` in this way makes the code more readable and easier to follow compared to traditional completion handler approaches. The `@MainActor` attribute ensures that UI updates happen on the main thread.

```swift
import Foundation
import SwiftUI

@MainActor
class DataViewModel: ObservableObject {
    @Published var data: String = ""
    @Published var isLoading: Bool = false
    @Published var errorMessage: String?
    
    private let fetchDataUseCase: FetchDataUseCaseProtocol
    
    init(fetchDataUseCase: FetchDataUseCaseProtocol = FetchDataUseCase()) {
        self.fetchDataUseCase = fetchDataUseCase
    }
    
    func loadData() async {
        isLoading = true
        errorMessage = nil
        
        do {
            let result = try await fetchDataUseCase.execute()
            data = result
        } catch {
            errorMessage = error.localizedDescription
        }
        
        isLoading = false
    }
}
```

### 5. View

Finally, the view observes the view model and updates the UI accordingly. `ContentView` observes `DataViewModel` and displays a loading indicator, the fetched data, or an error message based on the state of the view model.

```swift
import SwiftUI

struct ContentView: View {
    @StateObject private var viewModel = DataViewModel()
    
    var body: some View {
        VStack {
            if viewModel.isLoading {
                ProgressView()
            } else if let errorMessage = viewModel.errorMessage {
                Text("Error: \(errorMessage)")
            } else {
                Text(viewModel.data)
            }
        }
        .onAppear {
            Task {
                await viewModel.loadData()
            }
        }
        .padding()
    }
}
```