Sitemap

Clean Architecture + Swift 6

3 min readMar 5, 2025

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:

Clean-SwiftUI Medium 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!

--

--

Responses (1)