DEV Community

Tamas Dancsi
Tamas Dancsi

Posted on

Async/await and SwiftUI

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"
    }
}
Enter fullscreen mode Exit fullscreen mode

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()
    }
}
Enter fullscreen mode Exit fullscreen mode

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()
    }
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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()
    }
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)