[アーキテクチャ]Clean SWIFT(VIP)


本当にきれいではありませんか?

Clean Swift?

  • ViewController
  • Interactor
  • Presenter
  • Worker
  • Router
  • Models
  • Clean Architectureに基づいてIOSを再解釈した結果をClean SWIFTと呼ぶ.
    ViewController、Interactor、Presenterの最初のアルファベットによってVIPモードとも呼ばれます.

    公式サイトの画像で.
    最大の特徴は、元のデータストリームを生成することです.
    ViewController
    画面表示とユーザーインタラクションを担当
    Interactor
    業務ロジックを担当する
    Presenter
    Interactorから得られた結果をビューの表示可能な形式に変換する責任を負う
    Worker
    Interactorから複雑な論理を分離または拡張するために使用
    Router
    スクリーン移動を担当するロジック
    Models
    UseCaseとケース階層型データ構造の定義
    詳細についてはここをご確認ください.

    インプリメンテーション


    CLEAN SWIFTは、ファイルの分離によって役割や注目点を分離したいと考えています.
    この点は規模の小さい開発を行うと効率が低下する.
    しかし,ファイルに分離されるため,各階層の機能は明確に区別できる.

    ViewController


    イベントが発生すると、そのビジネスロジックを実装する関数が呼び出されます.
    func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
        interactor?.fetchRepositories(request: Search.FetchRepositories.Request(text: searchBar.text))
    }
    リストを更新するための関数を実装します.
    func displayRepositories(viewModel: Search.FetchRepositories.ViewModel) {
        DispatchQueue.main.async {
            self.displayedRepositories = viewModel.fetchedRepositories
            self.tableView.reloadData()
        }
    }

    Interactor


    ビジネスロジックを担当し、詳細はWorkerが処理します.
    func fetchRepositories(request: Search.FetchRepositories.Request) {
        worker = SearchWorker()
        worker?.fetchRepositories(text: request.text, completion: { result in
            switch result {
            case .success(let response):
                self.repositories = response.items
                self.presenter?.presentFetchedRepositories(response: response)
                    
            case .failure(let error):
                print("error : \(error.localizedDescription)")
                let response = Search.FetchRepositories.Response()
                self.repositories = response.items
                self.presenter?.presentFetchedRepositories(response: response)
            }
        })
    }

    Presenter


    Interactorから受信したデータをビューに変換して転送します.
    func presentFetchedRepositories(response: Search.FetchRepositories.Response) {
        let viewModel = Search.FetchRepositories.ViewModel(fetchedRepositories: response.items)
        self.viewController?.displayRepositories(viewModel: viewModel)
    }

    Worker


    インタラクティブで必要な詳細なビジネスロジックを実装します.
    func fetchRepositories(text: String?, completion: @escaping(Result<Search.FetchRepositories.Response, Error>) -> Void) {
        var urlComponents = URLComponents(string: "https://api.github.com/search/repositories")
        urlComponents?.queryItems = [
            URLQueryItem(name: "q", value: text)
        ]
            
        URLSession.shared.dataTask(with: (urlComponents?.url)!) { data, response, error in
            guard error == nil else {
                completion(.failure(error!))
                return
            }
                
            if let response = response as? HTTPURLResponse, response.statusCode == 200, let data = data {
                do {
                    let searchResponse = try JSONDecoder().decode(Search.FetchRepositories.Response.self, from: data)
                    completion(.success(searchResponse))
                } catch {
                    print("error : \(error.localizedDescription)")
                    completion(.failure(error))
                }
            }
        }.resume()
    }

    Router


    Segueにより画面移動やコード化が可能です.
    SwinjectのようなDIを使用すれば、ViewControllerのsetupを分離することができる.
    またpassData関数でScene間でデータを渡すこともできます.
    func routeToSearchDetail(segue: UIStoryboardSegue?) {
        if let segue = segue {
            let destinationVC = segue.destination as! SearchDetailViewController
            var destinationDS = destinationVC.router!.dataStore!
            passDataToSearchDetail(source: dataStore!, destination: &destinationDS)
        } else {
            let storyboard = UIStoryboard(name: "Main", bundle: nil)
            let destinationVC = storyboard.instantiateViewController(withIdentifier: "SearchDetailViewController") as! SearchDetailViewController
            var destinationDS = destinationVC.router!.dataStore!
            passDataToSearchDetail(source: dataStore!, destination: &destinationDS)
            navigateToSearchDetail(source: viewController!, destination: destinationVC)
        }
    }

    Models


    Sceneで使用するUserCaseを定義し、ケースの階層型データ構造を定義します.
    enum Search {
        // MARK: Use cases
        enum FetchRepositories {
            struct Request {
                let text: String?
            }
            struct Response: Codable {
                var total_count: Int = 0
                var incomplete_results: Bool = false
                var items: [Repository] = []
            }
            struct ViewModel {
                let fetchedRepositories: [Repository]
            }
        }
    }
    

    結果



    完全なコードは羽状バニラにあります.