アーキテクチャの作成:iOS Coordinatorパターンの使用



イラスト



遅かれ早かれ、各チームは独自のアーキテクチャアプローチの実装について考え始め、それについて多くのコピーが壊れました。 そのため、 Umbrella ITでは、柔軟なツールを使用したいと常に考えていました。そのため、アーキテクチャの形成に苦痛はなく、ナビゲーション、モックファイル、分離、およびテストの問題は恐ろしいものでした。生い茂ったプロジェクトにぶら下がっています。 幸いなことに、私たちは大げさな省略形を持つ新しい「排他的な」アーキテクチャについて話をしていません。 現在人気のあるアーキテクチャ(MVP、MVVM、VIPER、Clean-swift)はそれぞれのタスクに対処しており、このアプローチまたはそのアプローチの誤った選択と誤った使用のみが困難を引き起こすことを認めなければなりません。 ただし、採用されたアーキテクチャのフレームワーク内では、さまざまなパターンを使用できます。これにより、柔軟性、分離、テスト容易性、再利用など、ほとんど神話上の指標を達成できます。



もちろん、アプリケーションは異なります。 プロジェクトに直列に接続された少数の画面しか含まれていない場合、モジュール間の複雑な相互作用は特に必要ありません。 通常のセグエ接続を使用して、これらすべてを古き良きMVC / MVPで味付けすることは非常に可能です。 遅かれ早かれ、建築上のno索はすべての開発者を打ち負かしますが、それでも実装はプロジェクトの目標と複雑さに見合ったものでなければなりません。 そのため、プロジェクトに複雑な画面構造とさまざまな状態(承認、ゲストモード、オフライン、ユーザーの役割など)が含まれる場合、アーキテクチャへの単純化されたアプローチは確かにトリックを果たします:多くの依存関係、非自明で高価なデータ転送画面と状態、ナビゲーションの問題、そして最も重要なこと-これには柔軟性と再利用性がなく、ソリューションはプロジェクトにしっかりと溶け込み、画面Aは常に画面Bを開きます。同じ変更を試みると痛みを伴うリファクタリングにつながります ngamを使用すると、間違えてミスを犯したり、以前は機能していたものを壊したりすることが非常に簡単になります。 以下の例では、2つの状態を持つアプリケーションを柔軟に編成する方法を説明します。ユーザーは承認されていないため、承認画面に移動する必要があります。ユーザーは承認され、特定のメイン画面を開く必要があります。



1.主なプロトコルの実装



まず、ベースを実装する必要があります。 すべては、調整可能、提示可能、ルーティング可能なプロトコルから始まります。



protocol Coordinatable: class { func start() } protocol Presentable { var toPresent: UIViewController? { get } } extension UIViewController: Presentable { var toPresent: UIViewController? { return self } func showAlert(title: String, message: String? = nil) { UIAlertController.showAlert(title: title, message: message, inViewController: self, actionBlock: nil) } }
      
      





この例では、showAlertはUIViewController拡張機能にある通知を呼び出すための便利なメソッドです。



 protocol Routable: Presentable { func present(_ module: Presentable?) func present(_ module: Presentable?, animated: Bool) func push(_ module: Presentable?) func push(_ module: Presentable?, animated: Bool) func push(_ module: Presentable?, animated: Bool, completion: CompletionBlock?) func popModule() func popModule(animated: Bool) func dismissModule() func dismissModule(animated: Bool, completion: CompletionBlock?) func setRootModule(_ module: Presentable?) func setRootModule(_ module: Presentable?, hideBar: Bool) func popToRootModule(animated: Bool) }
      
      





2.コーディネーターを作成する



時々、アプリケーション画面を変更する必要があります。つまり、ダウンキャストせずに、SOLIDの原則に違反することなく、テストしたレイヤーを実装する必要があります。



座標レイヤーの実装に進みます。







アプリケーションを開始した後、AppCoordinatorメソッドを呼び出して、どのフローを開始するかを決定します。 たとえば、ユーザーが登録されている場合はフローアプリケーションを実行する必要があり、登録されていない場合はフロー承認を実行する必要があります。 この場合、MainCoordinatorとAuthorizationCoordinatorが必要です。 承認のコーディネーターについて説明しますが、他のすべての画面も同様の方法で作成できます。



まず、コーディネーターに出力を追加して、上位のコーディネーター(AppCoordinator)と接続できるようにする必要があります。



 protocol AuthorizationCoordinatorOutput: class { var finishFlow: CompletionBlock? { get set } } final class AuthorizationCoordinator: BaseCoordinator, AuthorizationCoordinatorOutput { var finishFlow: CompletionBlock? fileprivate let factory: AuthorizationFactoryProtocol fileprivate let router : Routable init(router: Routable, factory: AuthorizationFactoryProtocol) { self.router = router self.factory = factory } } // MARK:- Coordinatable extension AuthorizationCoordinator: Coordinatable { func start() { performFlow() } } // MARK:- Private methods private extension AuthorizationCoordinator { func performFlow() { //:- Will implement later } }
      
      









上記のように、ルーターとモジュールファクトリーを備えた承認コーディネーターがあります。 しかし、誰がいつstart()メソッドを呼び出すのでしょうか?

ここで、AppCoordinatorを実装する必要があります。



 final class AppCoordinator: BaseCoordinator { fileprivate let factory: CoordinatorFactoryProtocol fileprivate let router : Routable fileprivate let gateway = Gateway() init(router: Routable, factory: CoordinatorFactory) { self.router = router self.factory = factory } } // MARK:- Coordinatable extension AppCoordinator: Coordinatable { func start() { self.gateway.getState { [unowned self] (state) in switch state { case .authorization: self.performAuthorizationFlow() case .main: self.performMainFlow() } } } } // MARK:- Private methods func performAuthorizationFlow() { let coordinator = factory.makeAuthorizationCoordinator(with: router) coordinator.finishFlow = { [weak self, weak coordinator] in guard let `self` = self, let `coordinator` = coordinator else { return } self.removeDependency(coordinator) self.start() } addDependency(coordinator) coordinator.start() } func performMainFlow() { // MARK:- main flow logic }
      
      





この例から、AppCoordinatorにはルーター、コーディネーターファクトリ、およびAppCoordinatorのエントリポイントの状態があり、その役割はアプリケーションのフローの開始を決定することがわかります。



 final class CoordinatorFactory { fileprivate let modulesFactory = ModulesFactory() } extension CoordinatorFactory: CoordinatorFactoryProtocol { func makeAuthorizationCoordinator(with router: Routable) -> Coordinatable & AuthorizationCoordinatorOutput { return AuthorizationCoordinator(router: router, factory: modulesFactory) } }
      
      





3.工場コーディネーターの実施



各コーディネーターは、ルーターとモジュールファクトリーで初期化されます。 さらに、各コーディネーターは基本コーディネーターから継承する必要があります。



 class BaseCoordinator { var childCoordinators: [Coordinatable] = [] // Add only unique object func addDependency(_ coordinator: Coordinatable) { for element in childCoordinators { if element === coordinator { return } } childCoordinators.append(coordinator) } func removeDependency(_ coordinator: Coordinatable?) { guard childCoordinators.isEmpty == false, let coordinator = coordinator else { return } for (index, element) in childCoordinators.enumerated() { if element === coordinator { childCoordinators.remove(at: index) break } } } }
      
      





BaseCoordinator-子コーディネーターの配列と、コーディネーターの依存関係を削除および追加する2つのメソッドを含むクラス。



4. AppDelegateの構成



それでは、 UIApplicationMainの外観を見てみましょう



 @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? var rootController: UINavigationController { window?.rootViewController = UINavigationController() window?.rootViewController?.view.backgroundColor = .white return window?.rootViewController as! UINavigationController } fileprivate lazy var coordinator: Coordinatable = self.makeCoordinator() func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { coordinator.start() return true } } // MARK:- Private methods private extension AppDelegate { func makeCoordinator() -> Coordinatable { return AppCoordinator(router: Router(rootController: rootController), factory: CoordinatorFactory()) } }
      
      





デリゲートメソッドdidFinishLaunchingWithOptionsが呼び出されるとすぐに、AppCoordinatorのstart()メソッドが呼び出され、アプリケーションのさらなるロジックが決定されます。



5.画面モジュールの作成



次に何が起こるかを示すために、AuthorizationCoordinatorに戻ってperformFlow()メソッドを実装しましょう。



最初に、ModulesFactoryクラスにAuthorizationFactoryProtocolインターフェースを実装する必要があります。



 final class ModulesFactory {} // MARK:- AuthorizationFactoryProtocol extension ModulesFactory: AuthorizationFactoryProtocol { func makeEnterView() -> EnterViewProtocol { let view: EnterViewController = EnterViewController.controllerFromStoryboard(.authorization) EnterAssembly.assembly(with: view) return view
      
      





原則として、モジュールファクトリのメソッドを呼び出すことにより、ストーリーボードからViewControllerを初期化し、特定のアーキテクチャ(MVP、MVVM、CleanSwift)内でこのモジュールのすべての必要なコンポーネントをリンクします。



必要な準備が完了したら、AuthorizationCoordinatorのperformFlow()メソッドを実装できます。

このコーディネーター内の開始画面はEnterViewです。

モジュールファクトリを使用するperformFlow()メソッドでは、指定されたコーディネーターの既製モジュールの作成が呼び出され、View Controllerが一度に呼び出すクロージャーを処理するロジックが実装され、このモジュールは画面のナビゲーションスタックのルートによってルーターに設定されます:



 private extension AuthorizationCoordinator { func performFlow() { let enterView = factory.makeEnterView() finishFlow = enterView.onCompleteAuthorization enterView.output?.onAlert = { [unowned self] (message: String) in self.router.toPresent?.showAlert(message: message) } router.setRootModule(enterView) } }
      
      











一部の場所では複雑に見えますが、このパターンはモックファイルでの作業に理想的であり、モジュールを相互に完全に分離することができます。また、完全なテストカバレッジに適したUIKitから抽象化します。 同時に、コーディネーターはアプリケーションのアーキテクチャに厳しい要件を課すことはなく、便利な追加、構造化ナビゲーション、依存関係、モジュール間のデータフローにすぎません。



クリーンアーキテクチャに基づくデモと、必要なアーキテクチャレイヤーを作成するための便利なXcodeテンプレートを含むgithubリンクします。



All Articles