Clean Architecture in SwiftUI 5.5

Clean Architecture in SwiftUI 5.5

By employing clean architecture, you can design applications with very low coupling that is independent of technical implementation details. In that way, the application becomes easy to maintain, flexible to change and intrinsically testable. What to follow is a suggestion of how to structure a project in a clean architecture way. We're going to build an iOS to-do application using SwiftUI. We’ll only illustrate one use case of listing to-dos retrieved from an API.

Let’s get started.

The folder/group structure of the application takes on the following form:

├── Core 
├── Data 
├── Domain 
└── Presentation

Let’s start with the Domain Layer.

This layer describes WHAT your application does. Let me explain, Many applications are built and structured in a way that you cannot understand what the application does just by looking at the folder structure. Using a house building analogy, you can quickly identify the buildings looks and functionality by viewing the floor plan and elevation of the building

Floor Plan

In the same way, the domain layer of our application should specify and describe WHAT it does. In this folder, we would use models, repository interfaces, and use cases.

├── Core
├── Data
├── Presentation
└── Domain
    ├── Model
    │   ├── Todo.swift
    │   └── User.swift
    ├── Repository
    │   ├── TodoRepository.swift
    │   └── UserRepository.swift
    └── UseCase
        ├── Todo
        │   ├── GetTodos.swift
        │   ├── GetTodo.swift
        │   ├── DeleteTodo.swift
        │   ├── UpdateTodo.swift
        │   └── CreateTodo.swift
        └── User
            ├── GetUsers.swift
            ├── GetUser.swift
            ├── DeleteUser.swift
            ├── UpdateUser.swift
            └── CreateUser.swift
  1. Model: A model typically represents a real-world object that is related to the problem. In this folder, we would typically keep classes to represent objects. e.g. to-do, user, product, etc.
  2. Repository: Container for all repository interfaces. The repository is a central place to keep all model-specific operations. In this case, the to-do repository interface would describe repository methods. The actual repository implementation will be kept in the Data layer.
  3. UseCases: Container to list all functionality (business logic) of our application. e.g. Get to-dos, Delete to-do, Create to-do, Update to-do

The PRESENTATION layer will keep all the consumer-related code as to HOW the application will interact with the outside world. The presentation layer can be web forms, Command Line Interface, API Endpoints, etc. In this case, it would be the screens for a List of to-dos and its accompanying view model.

├── Core
├── Data
├── Domain
└── Presentation
    └── Todo
        └── TodoList
            ├── TodoListViewModel.swift
            └── TodoListView.swift

The DATA layer will keep all the external dependency-related code as to HOW they are implemented:

├── Core
├── Domain
├── Presentation
└── Data
    ├── Repository
    │   ├── TodoRepositoryImpl.swift
    └── DataSource
        ├── TodoDataSource.swift
        ├── API
        │   ├── TodoAPIDataSourceImpl.swift
        │   └── Entity
        │       ├── TodoAPIEntity.swift
        │       └── UserAPIEntity.swift
        └── DB
            ├── TodoDBDataSourceImpl.swift
            └── Entity
                ├── TodoDBEntity.swift
                └── UserDBEntity.swift
  1. Repository: Repository implementations
  2. DataSource: All data source interfaces and entities. An entity represents a single instance of your domain object saved into the database as a record. It has some attributes that we represent as columns in our DB tables or API endpoints. We can’t control how data is modelled on the external data source, so these entities are required to be mapped from entities to domain models in the implementations

and lastly, the CORE layer keep all the components that are common across all layers like constants or configs or dependency injection (which we won’t cover)

Our first task would be always to start with the domain models and data entities. Let’s start with the model

1import Foundation 2 3struct Todo: Identifiable { 4 let id: Int 5 let title: String 6 let isCompleted: Bool 7}

We need it to conform to Identifiable as we’re going to display these items in a list view.

Next let’s do the to-do entity

1import Foundation 2 3struct TodoAPIEntity: Codable { 4 let id: Int 5 let title: String 6 let completed: Bool 7}

Let’s now write an interface (protocol) for the to-do datasource

1 2import Foundation 3 4protocol TodoDataSource{ 5 6 func getTodos() async throws -> [Todo] 7 8}

We have enough to write an implementation of this protocol and call it TodoAPIImpl:

1import Foundation 2 3enum APIServiceError: Error{ 4 case badUrl, requestError, decodingError, statusNotOK 5} 6 7struct TodoAPIImpl: TodoDataSource{ 8 9 10 func getTodos() async throws -> [Todo] { 11 12 guard let url = URL(string: "\(Constants.BASE_URL)/todos") else{ 13 throw APIServiceError.badUrl 14 } 15 16 guard let (data, response) = try? await URLSession.shared.data(from: url) else{ 17 throw APIServiceError.requestError 18 } 19 20 guard let response = response as? HTTPURLResponse, response.statusCode == 200 else{ 21 throw APIServiceError.statusNotOK 22 } 23 24 guard let result = try? JSONDecoder().decode([TodoAPIEntity].self, from: data) else { 25 throw APIServiceError.decodingError 26 } 27 28 return result.map({ item in 29 Todo( 30 id: item.id, 31 title: item.title, 32 isCompleted: item.completed 33 ) 34 }) 35 } 36}

Note: this repository’s getTodos function returns a list of Todo. So, we have to map TodoEntity -> Todo:

Before we write our TodoRepositoryImpl let’s write the protocol for that in the Domain layer

1import Foundation 2 3protocol TodoRepository{ 4 5 func getTodos() async throws -> [Todo] 6 7}
1import Foundation 2 3struct TodoRepositoryImpl: TodoRepository{ 4 5 var dataSource: TodoDataSource 6 7 func getTodos() async throws -> [Todo] { 8 let _todos = try await dataSource.getTodos() 9 return _todos 10 } 11}

Now that we have our to-do repository, we can code up the GetTodos use case

1 2enum UseCaseError: Error{ 3 case networkError, decodingError 4} 5 6protocol GetTodos { 7 func execute() async -> Result<[Todo], UseCaseError> 8} 9 10import Foundation 11 12 13struct GetTodosUseCase: GetTodos{ 14 var repo: TodoRepository 15 16 func execute() async -> Result<[Todo], UseCaseError>{ 17 do{ 18 let todos = try await repo.getTodos() 19 return .success(todos) 20 }catch(let error){ 21 switch(error){ 22 case APIServiceError.decodingError: 23 return .failure(.decodingError) 24 default: 25 return .failure(.networkError) 26 } 27 } 28 } 29}

and then write our presentation’s view model and view

1 2import Foundation 3 4@MainActor 5class TodoListViewModel: ObservableObject { 6 7 var getTodosUseCase = GetTodosUseCase(repo: TodoRepositoryImpl(dataSource: TodoAPIImpl())) 8 @Published var todos: [Todo] = [] 9 @Published var errorMessage = "" 10 @Published var hasError = false 11 12 func getTodos() async { 13 errorMessage = "" 14 let result = await getTodosUseCase.execute() 15 switch result{ 16 case .success(let todos): 17 self.todos = todos 18 case .failure(let error): 19 self.todos = [] 20 errorMessage = error.localizedDescription 21 hasError = true 22 } 23 } 24}

Note: We use the @MainActor attribute for the view model class because we need to run these functions on the main thread, a singleton actor whose executor is equivalent to the main dispatch queue.

1import SwiftUI 2 3struct TodoListView: View { 4 @StateObject var vm = TodoListViewModel() 5 6 7 fileprivate func listRow(_ todo: Todo) -> some View { 8 HStack{ 9 Image(systemName: todo.isCompleted ? "checkmark.circle": "circle") 10 .foregroundColor(todo.isCompleted ? .green : .red) 11 Text("\(todo.title)") 12 } 13 } 14 15 fileprivate func TodoList() -> some View { 16 List { 17 ForEach(vm.todos){ item in 18 listRow(item) 19 } 20 } 21 .navigationTitle("Todo List") 22 .task { 23 await vm.getTodos() 24 } 25 .alert("Error", isPresented: $vm.hasError) { 26 } message: { 27 Text(vm.errorMessage) 28 } 29 } 30 31 var body: some View { 32 TodoList() 33 } 34} 35 36struct TodoListView_Previews: PreviewProvider { 37 static var previews: some View { 38 NavigationView{ 39 TodoListView() 40 }.navigationViewStyle(StackNavigationViewStyle()) 41 } 42}

Floor Plan

So to recap:

Floor Plan

Find code here: https://github.com/nanosoftonline/clean-architecture-swift