Clean Architecture + Swift 6
How to incorporate Swift 6 into Clean architecture?
First of all
If you are not yet familiar with this architecture, I recommend reading the previous article:
The updated code can be found in the public GitHub repository:
Clean-SwiftUI GitHub Repository
Disclaimer
I should have shared this a long time ago, but only now have I been able to dedicate myself to it. There is even a Pull Request on GitHub for you to help me with approvals and improvements.
What's in Swift 6?
In September 2024, Apple released Swift 6, which brought several improvements to app development, including improvements in concurrency and data isolation. In this article, we’ll cover the modifications made to the Clean architecture to take advantage of these new features.
Main Changes
1. Adoption of async/await
With Swift 6, concurrency has been improved, and the use of async/await
has become essential for asynchronous operations. The changes reflect this transition, making the code safer and more modern.
Before:
// ContentRepository.swift
protocol ContentRepository {
func fetchContents() -> [ContentEntity]
}
After:
// ContentRepository.swift
protocol ContentRepository {
func fetchContents() async throws -> [ContentEntity]
}
fetchContents()
now correctly handles asynchronous calls and can throw errors, ensuring more robust handling.
2. Struct to actor transformation
The new actors functionality in Swift 6 enables safe concurrency isolation to avoid race conditions.
Before:
// ContentRepositoryImpl.swift
struct ContentRepositoryImpl: ContentRepository {
var datasource: ContentDatasource
func fetchContents() -> [ContentEntity] {
let contents: [ContentModel] = datasource.fetchContents()
return contents.map({ ContentMapper.toEntity(from: $0) })
}
}
After:
// ContentRepositoryImpl.swift
actor ContentRepositoryImpl: ContentRepository {
private let datasource: ContentDatasource
init(datasource: ContentDatasource) {
self.datasource = datasource
}
func fetchContents() async throws -> [ContentEntity] {
let contents: [ContentModel] = try await datasource.fetchContents()
return contents.map({ ContentMapper.toEntity(from: $0) })
}
}
Now, ContentRepositoryImpl
is an actor
, ensuring security in concurrency and eliminating risks of simultaneous access to datasource
.
3. ViewModel update
The ContentViewModel
has been modified to use @MainActor
, ensuring that interface updates occur on the main thread.
Before:
// ContentViewModel.swift
struct ContentViewModel {
private var _fetchContentsUseCase: FetchContentsUseCase
func fetchContents() -> [ContentEntity] {
return _fetchContentsUseCase.call()
}
}
After:
// ContentViewModel.swift
import Foundation
@MainActor
class ContentViewModel: ObservableObject {
@Published var contents: [ContentEntity] = []
private var _fetchContentsUseCase: FetchContentsUseCase
init(_ fetchContentsUseCase: FetchContentsUseCase) {
self._fetchContentsUseCase = fetchContentsUseCase
}
func fetchContents() async throws {
do {
let data = try await _fetchContentsUseCase.call()
self.contents = data
} catch {
throw error
}
}
}
The class now inherits ObservableObject
, allowing the interface to react to changes in the contents
property.
4. ContentView update
ContentView
has also been adapted to use async/await
and StateObject
, ensuring better management of the ViewModel
lifecycle.
Before:
// ContentView.swift
@State var contents: [ContentEntity] = []
var body: some View {
HStack {
List {
ForEach(contents, id: \ .url) { content in
Text(content.theme)
}
}
}
.task {
contents = viewModel.fetchContents()
}
}
After:
// ContentView.swift
@StateObject private var viewModel: ContentViewModel
init() {
let viewModel = DependencyContainer().features.contentFeature.contentViewModel
_viewModel = StateObject(wrappedValue: viewModel)
}
var body: some View {
HStack {
List {
ForEach(viewModel.contents, id: \ .url) { content in
Text(content.theme)
.foregroundStyle(Color.blue)
}
}
}
.task(priority: .background) {
do {
try await viewModel.fetchContents()
} catch {
print(error)
}
}
}
Now the ViewModel
is correctly initialized as a StateObject
, and fetchContents()
is called inside .task()
using an optimized priority level.
Conclusion
Updating to Swift 6 has brought significant benefits to the Clean architecture in SwiftUI. The adoption of async/await
, the use of actor
for repositories, and the@MainActor
tag ensure a safer and more efficient structure. These changes reduce concurrency risks, improve the responsiveness of the app, and bring the code more in line with current best practices.
If you want to check out the updated code, access the repository on GitHub:
New changes: Clean-SwiftUI GitHub Repository
Do you have any questions or suggestions? Leave your comment below!