実際の「iOS」プロジェクトで「コーディネーター」を使用した経験

現代のプログラミングの世界はトレンドに富んでおり、これは「iOS」アプリケーションのプログラミングの世界にも当てはまります。 ここ数年で最も「ファッショナブルな」アーキテクチャパターンの1つが「コーディネーター」であると断言することをあまり間違えないことを願っています。 そのため、私たちのチームは少し前に、このテクニックを自分で試してみたいという非常に魅力的な欲求に気付きました。 さらに、非常に良いケースが見つかりました-ロジックの大幅な変更と、アプリケーションのナビゲーションの全体的な再計画。

問題


多くの場合、コントローラーはあまりにも多くのことを始めます: UINavigationControllerに直接「コマンドを与え」、「兄弟」コントローラーと「通信」します(それらを初期化してナビゲーションスタックに渡します)-一般に、やるべきことがたくさんあります彼らは疑うべきではありません。

これを回避する方法の1つは、まさに「コーディネーター」です。 さらに、判明したように、作業は非常に便利で非常に柔軟性があります。テンプレートは、小さなモジュール(おそらく1つの画面のみを表す)とアプリケーション全体(相対的に「フロー」を起動して、 UIApplicationDelegate )。

物語


Martin Fowlerは 、彼の著書 『 Patterns of Enterprise Application Architecture』で、このパターンをApplication Controllerと呼びました。 そして、「iOS」環境での彼の最初のポピュラーは、 Sorush Khanluと考えられています 。それはすべて、2015年の「NSSpain」に関するレポートから始まりまし 。 その後、 彼のサイトにレビュー記事掲載されました 。これにはいくつかの続編がありました(たとえば、 これ )。

その後、多くのレビューが行われました(「iosコーディネーター」クエリは、さまざまな品質と詳細度の多数の結果を提供します)。 「大規模」コントローラー。

先を見据えて、最も注目すべきトピックはUINavigationController戻るボタンの問題です。クリックはコードではなく処理されますが、 コールバックのみを取得できます

実際、なぜこれが問題なのですか? コーディネーターは、他のオブジェクトと同様に、メモリー内に存在するために、それらを「所有」するために他のオブジェクトを必要とします。 原則として、コーディネーターを使用してナビゲーションシステムを構築する場合、一部のコーディネーターは他のコーディネーターを生成し、それらへの強力なリンクを維持します。 発信元コーディネーターの「責任ゾーンを離れる」と、制御は発信元コーディネーターに戻り、発信元が占有していたメモリを解放する必要があります。

Sorushはこの問題を解決するための彼自身のビジョンを持っていてまた、いくつかの価値ある アプローチに注意します。 しかし、これに戻ります。

最初のアプローチ


実際のコードを表示する前に、原則はプロジェクトで出てきたものと完全に一致していますが、コードからの抜粋とその使用例は、知覚に干渉しない場所であればどこでも簡略化および削減されることを明確にしたいと思います。

最初にチームのコーディネーターで実験を始めたとき、このための時間と行動の自由はあまりありませんでした。既存の原則とナビゲーションデバイスを考慮する必要がありました。 コーディネーターの最初の実装オプションは、 UINavigationControllerが所有および管理する共通の「ルーター」に基づいていました。 彼は、ナビゲーションに必要なUIViewControllerインスタンスをどのように処理するかを知っています-プッシュ/ポップ、提示/ UIViewControllerUIViewController ルートコントローラーでの操作 。 そのようなルーターのインターフェースの例:

 import UIKit protocol Router { func present(_ module: UIViewController, animated: Bool) func dismissModule(animated: Bool, completion: (() -> Void)?) func push(_ module: UIViewController, animated: Bool, completion: (() -> Void)?) func popModule(animated: Bool) func setAsRoot(_ module: UIViewController) func popToRootModule(animated: Bool) } 

特定の実装はUINavigationControllerインスタンスで初期化され、それ自体には特にUINavigationControllerにくいものは何も含まれていません。 唯一の制限: UINavigationController他のインスタンスをインターフェイスメソッドへの引数の値として渡すことはできません(明らかな理由: UINavigationControllerはスタックにUINavigationControllerを含めることができません-これはUIKit制限です)。

コーディネーターは、他のオブジェクトと同様に、所有者、つまりリンクを保存する別のオブジェクトが必要です。 ルートへのリンクは、それを生成するオブジェクトによって保存できますが、各コーディネーターは他のコーディネーターも生成できます。 したがって、生成されたコーディネーターに管理メカニズムを提供するために、基本インターフェースが作成されました。

 class Coordinator { private var childCoordinators = [Coordinator]() func add(dependency coordinator: Coordinator) { // ... } func remove(dependency coordinator: Coordinator) { // ... } } 

コーディネーターの暗黙の利点の1つは、 UIViewController特定のサブクラスに関する知識のカプセル化です。 ルーターとコーディネーターの相互作用を確保するために、次のインターフェイスを導入しました。

 protocol Presentable { func presented() -> UIViewController } 

次に、特定の各コーディネーターはCoordinatorから継承し、 Presentableインターフェースを実装する必要があり、ルーターインターフェースは次の形式を取る必要があります。

 protocol Router { func present(_ module: Presentable, animated: Bool) func dismissModule(animated: Bool, completion: (() -> Void)?) func push(_ module: Presentable, animated: Bool, completion: (() -> Void)?) func popModule(animated: Bool) func setAsRoot(_ module: Presentable) func popToRootModule(animated: Bool) } 

Presentableを使用したアプローチでは、 UIViewControllerインスタンス(モジュール)に徹底的な処理を行わずに直接対話するように記述されたモジュール内でコーディネーターを使用することもできます。)

このすべての動作の簡単な例:

 final class FirstCoordinator: Coordinator, Presentable { func presented() -> UIViewController { return UIViewController() } } final class SecondCoordinator: Coordinator, Presentable { func presented() -> UIViewController { return UIViewController() } } let nc = UINavigationController() let router = RouterImpl(navigationController: nc) // Router implementation. router.setAsRoot(FirstCoordinator()) router.push(SecondCoordinator(), animated: true, completion: nil) router.popToRootModule(animated: true) 

次の近似


そしてある日、ナビゲーションの完全な変更と絶対的な表現の自由の瞬間がやってきました! 切望されたstart()メソッドを使用してコーディネーターにナビゲーションを実装しようとするのを妨げるものは何もありませんでした-元々そのシンプルさと簡潔さで魅了されたバージョンです。

上記のCoordinator機能は明らかに不要ではありません。 ただし、一般的なインターフェイスに同じメソッドを追加する必要があります。

 protocol Coordinator { func add(dependency coordinator: Coordinator) func remove(dependency coordinator: Coordinator) func start() } class BaseCoordinator: Coordinator { private var childCoordinators = [Coordinator]() func add(dependency coordinator: Coordinator) { // ... } func remove(dependency coordinator: Coordinator) { // ... } func start() { } } 

「Swift」は、抽象クラスを宣言する機能を提供しません(より古典的なオブジェクト指向のアプローチよりもプロトコル指向のアプローチを指向しているため)。したがって、 start()メソッドは空の実装のままにするか、押し込むことができますfatalError(_:file:line:)ようなものがあります(相続人でこのメソッドを強制的にオーバーライドします)。 個人的には、私は最初のオプションを好みます。

しかし、Swiftにはデフォルトの実装メソッドをプロトコルメソッドに追加する絶好の機会があります。そのため、もちろん最初の考えは基本クラスを宣言するのではなく、次のようなことをすることでした。

 extension Coordinator { func add(dependency coordinator: Coordinator) { // ... } func remove(dependency coordinator: Coordinator) { // ... } } 

ただし、プロトコル拡張機能では格納フィールドを宣言できません。これら2つのメソッドの実装は、プライベートな格納型プロパティに基づいていることは明らかです。

特定のコーディネーターの基礎は次のようになります。

 final class SomeCoordinator: BaseCoordinator { override func start() { // ... } } 

コーディネーターが機能するために必要な依存関係はすべて、イニシャライザーに追加できます。 典型的な例として、 UINavigationControllerインスタンス。

これがルートUIViewControllerマッピングを担当するルートコーディネーターである場合、コーディネーターは、たとえば、空のスタックを持つUINavigationController新しいインスタンスを受け入れることができます。

イベントを処理する場合(詳細は後述)、コーディネーターはこのUINavigationControllerを生成する他のコーディネーターにさらに渡すことができます。 また、現在のナビゲーションの状態である「プッシュ」、「プレゼン」、および少なくともナビゲーションスタック全体を置き換えることもできます。

可能なインターフェースの改善


後で判明したように、すべてのコーディネーターが他のコーディネーターを生成するわけではないため、すべてのコーディネーターがそのような基本クラスに依存する必要はありません。 したがって、隣接チームの同僚の1人は、継承を削除し、依存関係マネージャーインターフェイスを外部依存関係として導入することを提案しました。

 protocol CoordinatorDependencies { func add(dependency coordinator: Coordinator) func remove(dependency coordinator: Coordinator) } final class DefaultCoordinatorDependencies: CoordinatorDependencies { private let dependencies = [Coordinator]() func add(dependency coordinator: Coordinator) { // ... } func remove(dependency coordinator: Coordinator) { // ... } } final class SomeCoordinator: Coordinator { private let dependencies: CoordinatorDependencies init(dependenciesManager: CoordinatorDependencies = DefaultCoordinatorDependencies()) { dependencies = dependenciesManager } func start() { // ... } } 

ユーザー生成イベントの処理


さて、コーディネーターは新しいマッピングを作成し、何らかの形で開始しました。 ほとんどの場合、ユーザーは画面を見て、やり取りできる特定の視覚要素セット(ボタン、テキストフィールドなど)を確認します。それらの一部はナビゲーションイベントを引き起こします。 この問題を解決するために、従来の委任を使用します。

UIViewControllerサブクラスがあると仮定しUIViewController

 final class SomeViewController: UIViewController { } 

そして、それをスタックに追加するコーディネーター:

 final class SomeCoordinator: Coordinator { private let dependencies: CoordinatorDependencies private weak var navigationController: UINavigationController? init(navigationController: UINavigationController, dependenciesManager: CoordinatorDependencies = DefaultCoordinatorDependencies()) { self.navigationController = navigationController dependencies = dependenciesManager } func start() { let vc = SomeViewController() navigationController?.pushViewController(vc, animated: true) } } 

対応するコントローラーイベントの処理を同じコーディネーターに委任します。 ここでは、実際には、古典的なスキームが使用されます。

 protocol SomeViewControllerRoute: class { func onSomeEvent() } final class SomeViewController: UIViewController { private weak var route: SomeViewControllerRoute? init(route: SomeViewControllerRoute) { self.route = route super.init(nibName: nil, bundle: nil) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } @IBAction private func buttonAction() { route?.onSomeEvent() } } final class SomeCoordinator: Coordinator { private let dependencies: CoordinatorDependencies private weak var navigationController: UINavigationController? init(navigationController: UINavigationController, dependenciesManager: CoordinatorDependencies = DefaultCoordinatorDependencies()) { self.navigationController = navigationController dependencies = dependenciesManager } func start() { let vc = SomeViewController(route: self) navigationController?.pushViewController(vc, animated: true) } } extension SomeCoordinator: SomeViewControllerRoute { func onSomeEvent() { // ... } } 

戻るボタンの取り扱い


議論された建築テンプレートの別の優れたレビューが、Paul HudsonのWebサイト「Hacking with Swift」で公開されました 。 また、前述のリターンボタンの問題に対する可能な解決策の1つについての簡単でUINavigationController説明も含まれています。コーディネーター(必要な場合)は、 UINavigationController渡されたUINavigationControllerインスタンスのデリゲートを宣言し、関心のあるイベントを監視します。

このアプローチにはわずかな欠点がありますUINavigationController NSObjectのみがUINavigationControllerデリゲートになれます。

そのため、別のコーディネーターを作成するコーディネーターがいます。 これは、 start()呼び出すstart()によりstart()何らかのUIViewControllerUINavigationControllerスタックにUINavigationControllerます。 UINavigationBar [戻る]ボタンを押すと、生成されたコーディネーターが作業を完了したことを元のコーディネーターに知らせるだけで済みます(「フロー」)。 これを行うために、別の委任ツールを導入しました。生成された各コーディネーターにデリゲートが割り当てられ、そのインターフェースは生成コーディネーターによって実装されます。

 protocol CoordinatorFlowListener: class { func onFlowFinished(coordinator: Coordinator) } final class MainCoordinator: NSObject, Coordinator { private let dependencies: CoordinatorDependencies private let navigationController: UINavigationController init(navigationController: UINavigationController, dependenciesManager: CoordinatorDependencies = DefaultCoordinatorDependencies()) { self.navigationController = navigationController dependencies = dependenciesManager super.init() } func start() { let someCoordinator = SomeCoordinator(navigationController: navigationController, flowListener: self) dependencies.add(someCoordinator) someCoordinator.start() } } extension MainCoordinator: CoordinatorFlowListener { func onFlowFinished(coordinator: Coordinator) { dependencies.remove(coordinator) // ... } } final class SomeCoordinator: NSObject, Coordinator { private weak var flowListener: CoordinatorFlowListener? private weak var navigationController: UINavigationController? init(navigationController: UINavigationController, flowListener: CoordinatorFlowListener) { self.navigationController = navigationController self.flowListener = flowListener } func start() { // ... } } extension SomeCoordinator: UINavigationControllerDelegate { func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) { guard let fromVC = navigationController.transitionCoordinator?.viewController(forKey: .from) else { return } if navigationController.viewControllers.contains(fromVC) { return } if fromVC is SomeViewController { flowListener?.onFlowFinished(coordinator: self) } } } 

上記の例では、 MainCoordinatorは何もMainCoordinatorません。別のコーディネーターの「フロー」を起動するだけです-実際には、もちろん役に立たないのです。 このアプリケーションでは、 MainCoordinatorは外部からデータを受信し、それに応じて、アプリケーションの状態(承認済み、未承認など)を判断します。 -どの画面を表示する必要があるか。 これに応じて、対応するコーディネーターのフローを起動します。 発信元のコーディネーターが作業を完了すると、メインコーディネーターはCoordinatorFlowListenerを介してこれに関する信号を受信し、たとえば、別のコーディネーターのフローを起動します。

おわりに


もちろん、慣れたソリューションには、いくつかの欠点があります(問題の解決策など)。

はい、多くの委任を使用する必要がありますが、単純であり、生成から生成へ(コントローラーからコーディネーターへ、生成されたコーディネーターから生成へ)という単一の方向性があります。

はい。メモリリークを回避するには、各コーディネーターにほぼ同一の実装を備えたUINavigationControllerデリゲートメソッドを追加する必要があります。 (最初のアプローチにはこの欠点はありませんが、代わりに特定のコーディネーターの任命に関する内部知識をより寛大に共有します。)

しかし、このアプローチの最大の欠点は、実生活ではコーディネーターが残念ながら自分の周りの世界について私たちが望んでいるよりも少し多く知っていることです。 より正確には、コーディネーターが直接認識していない外部条件に依存する論理要素を追加する必要があります。 基本的に、これは実際には、 start()メソッドがonFlowFinished(coordinator:)とき、またはonFlowFinished(coordinator:)コールバックがonFlowFinished(coordinator:)ときに起こることです。 そして、これらの場所では何でも起こり得、常に「ハードコーディングされた」動作になります。スタックへのコントローラーの追加、スタックの交換、ルートコントローラーへの戻りなど、何でも。 そしてそれはすべて、現在のコントローラーの能力ではなく、外部条件に依存します。

それにもかかわらず、コードは「きれい」で簡潔であり、それを使用するのは本当に素晴らしいことであり、コード内のナビゲーションははるかに簡単です。 言及された短所では、それらを認識して、存在する可能性が非常に高いように思われました。
この場所を読んでくれてありがとう! 彼らが自分自身に役立つ何かを学んだことを願っています。 そして、突然「私よりも多く」を望むなら、ここに私のTwitterへリンクがあります。

Source: https://habr.com/ru/post/J444038/


All Articles