Swiftui 5.5を使用してクリーンアーキテクチャ


Clean Architectureを使用することにより、非常に低い結合とデータベースやフレームワークなどの技術的な実装の詳細から独立したアプリケーションを設計できます.そのように、アプリケーションを維持し、柔軟に変更することが容易になります.また、それは本質的にテスト可能になります.ここでは、クリーンアーキテクチャプロジェクトの仕組みを紹介します.この時間iSwipを使用してアプリケーションを構築する予定です.
プロジェクトのフォルダー/グループ構造は次のようになります.
├── Core
├── Data
├── Domain
└── Presentation
ドメイン層から始めましょう.
このレイヤは、プロジェクト/アプリケーションが何をするかを示します.説明しましょう、多くのアプリケーションが構築され、どのようなアプリケーションがフォルダ構造を見ているかを理解できない方法で構造化されます.家の類推の建物を使用して、建物のフロアプランと建物の高さを表示することによってその建物のように見える機能を迅速に識別することができます

同様に、我々のプロジェクトのドメイン層はアプリケーションが何をするかを指定して、記述するべきです.このフォルダでは、モデル、リポジトリインターフェイス、ユースケースを使い続けます.
├── 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


  • モデル:典型的には、問題に関連する現実世界のオブジェクトを表します.このフォルダでは、通常、オブジェクトを表すクラスを保持します.例えばTODO , User , etc

  • リポジトリ:すべてのリポジトリインターフェイス用のコンテナー.リポジトリはすべてのモデル固有の操作を維持する中心的な場所です.この場合、TODOリポジトリインターフェイスはリポジトリメソッドを説明します.実際のリポジトリの実装はデータ層に保持されます.

  • UseCases :アプリケーションのすべての機能を一覧表示するコンテナー.取得、削除、Countを作成、更新
  • プレゼンテーション層は、アプリケーションが外部世界とどのように相互作用するかについて、消費者関連のコードの全てを維持するでしょう.プレゼンテーション層は、WebForm、コマンドラインインターフェイス、APIエンドポイントなどです.この場合、それはToDOSのリストとそれに付随するビューモデルになります.
    ├── Core
    ├── Data
    ├── Domain
    └── Presentation
        └── Todo
            └── TodoList
                ├── TodoListViewModel.swift
                └── TodoListView.swift
    
    データ層は、外部依存関係の関連付けられたコードをどのように実装するかを維持します.
    ├── Core
    ├── Domain
    ├── Presentation
    ├── Data
        ├── Repository
        │   ├── TodoRepositoryImpl.swift
        │   ├── TodoAPIDataSourceImpl.swift
        │   └── TodoDBDataSourceImpl.swift
        └── DataSource
            ├── API
            │   ├── TodoAPIDataSource.swift
            │   └── Entity
            │       ├── TodoAPIEntity.swift
            │       └── UserAPIEntity.swift
            └── DB
                ├── TodoDBDataSource.swift
                └── Entity
                    ├── TodoDBEntity.swift
                    └── UserDBEntity.swift
    

  • リポジトリ:リポジトリの実装

  • DataSource :すべてのデータソースインターフェイスとエンティティ.エンティティは、データベースとしてデータベースに保存されたドメインオブジェクトの単一のインスタンスを表します.DBテーブルまたはAPIエンドポイントの列として表現する属性があります.データが外部データソースにどのようにモデル化されるかを制御することはできません.したがって、これらの実体は、実装から
  • における実体モデルにエンティティからマップされる必要があります
    そして最後に、コア層は、定数やコンフィグや依存関係の注入のようなすべての層に共通しているすべてのコンポーネントを保持します
    我々の最初のタスクは、常にドメインモデルとデータエンティティ
    struct Todo: Identifiable {
        let id: Int
        let title: String
        let isCompleted: Bool
    }
    
    我々はリストビューでこれらのアイテムを表示しようとしているように識別可能に一致するようにする必要があります.
    次はtodoエンティティを行いましょう
    
    struct TodoEntity: Codable {
        let id: Int
        let title: String
        let completed: Bool
    }
    
    さあ、tododatasourceのためにインターフェースを書きましょう
    protocol TodoDataSource{    
        func getTodos() async throws -> [Todo]    
    }
    
    このプロトコルの実装を書くには十分にあります.
    enum APIServiceError: Error{
        case badUrl, requestError, decodingError, statusNotOK
    }
    
    struct TodoAPIImpl: TodoDataSource{
    
    
        func getTodos() async throws -> [Todo] {
    
            guard let url = URL(string:  "\(Constants.BASE_URL)/todos") else{
                throw APIServiceError.badUrl
            }
    
            guard let (data, response) = try? await URLSession.shared.data(from: url) else{
                throw APIServiceError.requestError
            }
    
            guard let response = response as? HTTPURLResponse, response.statusCode == 200 else{
                throw APIServiceError.statusNotOK
            }
    
            guard let result = try? JSONDecoder().decode([TodoEntity].self, from: data) else {
                throw APIServiceError.decodingError
            }
    
            return result.map({ todoEntity in
                Todo(
                    id: todoEntity.id,
                    title: todoEntity.title,
                    isCompleted: todoEntity.completed
                )
            })
        }
    }
    
    注意:このリポジトリのgetTodos関数は、todoのリストを返します.したがって、ToDoEntity -> ToDoをマップする必要があります.
    我々のtodorepositoryimplを書く前に、ドメイン層でそれのためにプロトコルを書きましょう
    protocol TodoRepository{ 
        func getTodos() async throws -> [Todo] 
    }
    
    struct TodoRepositoryImpl: TodoRepository{ 
        var api: TodoAPI 
        func getTodos() async throws -> [Todo] {
            let _todos =  try await api.getTodos()
            return _todos
        }
    }
    
    TODOリポジトリを持っているので、getTodosユースケースをコード化できます
    enum UseCaseError: Error{
        case networkError, decodingError
    }
    
    struct GetTodosUseCase{
        var repo: TodoRepository
    
        func execute() async -> Result<[Todo], UseCaseError>{
            do{
                let todos = try await repo.getTodos()
                return .success(todos)
            }catch(let error){
                switch(error){
                case APIServiceError.decodingError:
                    return .failure(.decodingError)
                default:
                    return .failure(.networkError)
                }
            }
        }
    }
    
    そして、順番にプレゼンテーションのビューモデルとビューを書くことができます
    @MainActor
    class TodoListViewModel: ObservableObject {
    
        var getTodosUseCase = GetTodosUseCase(repo: TodoRepositoryImpl(api: TodoAPIImpl()))
        @Published var todos: [Todo] = []
        @Published var errorMessage = ""
        @Published var hasError = false
    
        func getTodos() async {
            errorMessage = ""
            let result = await getTodosUseCase.execute()
            switch result{
            case .success(let todos):
                self.todos = todos
            case .failure(let error):
                self.todos = []
                errorMessage = error.localizedDescription
                hasError = true
            }
        }
    }
    
    注意: View Modelクラスの@ mainActor属性を使用します.なぜなら、これらの関数をメインスレッドに実行する必要があるためです.
    struct TodoListView: View {
        @StateObject var vm = TodoListViewModel()
    
    
        fileprivate func listRow(_ todo: Todo) -> some View {
            HStack{
                Image(systemName: todo.isCompleted ? "checkmark.circle": "circle")
                    .foregroundColor(todo.isCompleted ? .green : .red)
                Text("\(todo.title)")
            }
        }
    
        fileprivate func TodoList() -> some View {
            List {
                ForEach(vm.todos){ item in
                    listRow(item)
                }
            }
            .navigationTitle("Todo List")
            .task {
               await vm.getTodos()
            }
            .alert("Error", isPresented: $vm.hasError) {
            } message: {
                Text(vm.errorMessage)
            }
        }
    
        var body: some View {
           TodoList()
        }
    }
    
    

    以下に要約する.