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.
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.
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.
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.
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.
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.
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()
}
}
Top comments (0)