LoginSignup
5
3

[Swift]Coordinatorパターンを学ぶ過程を記録してみた

Last updated at Posted at 2023-06-19

始めに

下記点において修正する方法はないか気になった。

  • push/presentをViewControllerで実装しない
  • 次のViewControllerを知らないようにする

調べてみたところ、Coordinatorパターンを見つけたので実装してみる。

Coordinatorパターンとは?

ChatGPTに聞いたらわかりやすかったので記録。

Coordinatorパターンとは、主にiOS開発で用いられるアーキテクチャのパターンの一つで、アプリケーションのナビゲーションフローを管理し、View Controllersが直接通信することを避けることを目的としています。Swift言語において、このパターンはコードの分離と再利用を容易にし、アプリケーションのテスト性を向上させるのに役立ちます。

Coordinatorパターンの基本的な概念は、各View Controllerが自身のナビゲーションロジックを持たず、その役割を専用のコーディネーターに委任することです。コーディネーターは、アプリケーション内で移動するためのナビゲーションロジックをカプセル化し、特定のView Controllerが他のView Controllerに依存しないようにします。

具体的な利点としては、以下のようなものがあります:

  1. 単一責任の原則:Coordinatorパターンを用いると、View ControllerはUIとユーザインタラクションの管理に専念し、ナビゲーションロジックはCoordinatorが担当します。これにより、コードの責任が分離され、より読みやすく、保守しやすいコードが生まれます。

  2. 再利用性:View Controllers間でナビゲーションロジックを共有する代わりに、それぞれのCoordinatorがそれぞれのナビゲーションフローを管理するため、特定のView Controllerを異なるコンテキストで再利用することが容易になります。

  3. テスタビリティ:Coordinatorがナビゲーションロジックを担当することで、単体テストが容易になります。特定の入力に対して期待するナビゲーションアクションが行われるかをテストすることが可能となります。

SwiftのCoordinatorパターンは、効率的でスケーラブルなコードを書くのに役立つ強力なパターンです。しかし、その導入と実装はアプリケーションの規模と要件によります。適切な場合にはこのパターンを活用することで、アプリケーションの設計と保守性を大幅に改善することができます。

Coordinatorパターンの詳細については、下記を参考にする。
TabBarControllerをrootViewControllerに設定している例が載っている。とてもわかりやすい!

下記のわかりやすかった!

実際にコードを書いていく

実践1

NavigationControllerを使用して、FirstVCを表示する。

完成図

1. Coordinatorプロトコルを定義する。

AppCoordinator.swift
protocol Coordinator {
    func start()
}

Coordinatorプロトコルを定義することで、Coordinatorを適用する全てのクラスが、startメソッドを持つことが保証され、一貫した操作が可能になる。

2. 起動時に表示するFirstViewControllerを作成する。
3. FirstViewControllerと紐づく、FirstCoordinatorを作成する。

FirstCoordinator.swift
import UIKit

// ①
final class FirstCoordinator: Coordinator {
    
    private let navigator: UINavigationController
    private var firstViewController: FirstViewController?
    // ②
    init(navigator: UINavigationController) {
        self.navigator = navigator
    }
    
    // ③
    func start() {
        let vc = FirstViewController()
        self.navigator.viewControllers = [vc]
        self.firstViewController = vc
    }
    
}

①については、プロトコルを継承し、一貫した操作を可能にしている。②については、初期化時にUINavigationControllerを受け取る。FirtViewControllerの場合、起動時に作成されたUINavigationoControllerに対してViewControllerを変更していく。③については、FirstViewControllerを初期化して、NaivagationControllerに表示するメソッドを定義している。初期化したFirstViewControllerの寿命はFirstCoordinatorと一緒にしたいので、プロパティとして保持する。

4. Application Coordinator(AppCoordinator)クラスを作成する。
このクラスは、SceneDelegateが所有し、ルートビューに対するCoordinatorとなる。つまり、AppCoordinatorはアプリケーション全体のナビゲーションフローを制御する役割を持つクラスで、SceneDelegateが生きている限り、存在し続ける。

Application Coordinatorをルートとして、1つのViewControllerにつき、1つのCoordinatorが存在する。そして、画面遷移の経路に沿った親子関係を構築する。

AppCoordinator.swift
final class AppCoordinator: Coordinator {
    
    private let window: UIWindow
    private let rootViewController: UINavigationController
    // ①
    private var firstCoordinator: FirstCoordinator
    
    init(window: UIWindow) {
        self.window = window
        rootViewController = .init()
        // ②
        firstCoordinator = FirstCoordinator(navigator: rootViewController)
    }
    
    func start() {
        // ③
        firstCoordinator.start()
        // ④
        window.rootViewController = rootViewController
        window.makeKeyAndVisible()
    }

}

①については、初期画面で表示するFirstViewControllerと紐づくCoordinatorになる。遷移先のCoordinatorを保持することで親子関係を作成していると意識すると、流れがつかみやすい。②については、使用するNavigationControolerを子に渡している。③については、FirestViewControllerを初期化してNavigationControllerに表示する処理を実行している。④については、起動時画面を設定している。この箇所の設定については、コードで起動時画面を表示する処理がわかっていれば理解できるので、以下を参考にする。

5. SceneDelegateを設定する。
SeceneDelegateにて、初期画面の設定をする。

SceneDelegate.swift
class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?
    // ①
    private var appCoordinator: AppCoordinator?
    
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {

        guard let _ = (scene as? UIWindowScene) else { return }
        
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            self.window = window
            
            let appCoordinator = AppCoordinator(window: window)
            // ②
            appCoordinator.start()
            self.appCoordinator = appCoordinator
        }
    }
}

①について、AppCoordinatorはアプリケーション全体のナビゲーションフローを制御する役割を持つクラスで、SceneDelegateが生きている限り、存在し続けるため、プロパティとして保持する。②について、rootViewControllerを設定する処理を実行している。

6. ビルドして表示!

実践2

実践1で作成したものに対して、FirstVCにボタンを設置し、タップするとSecondVCに遷移する。

完成図
  
1. SecondViewControllerを作成する。
2. SecondCoordinatorを作成する。

SecondCoordinator.swift
import UIKit

final class SecondCoordinator: Coordinator {
    
    private let navigator: UINavigationController
    private var secondViewController: SecondViewController?
    
    init(navigator: UINavigationController) {
        self.navigator = navigator
    }
    
    func start() {
        let vc = SecondViewController()
        // ①
        navigator.pushViewController(vc, animated: true)
        secondViewController = vc
    }
    
}

SecondCoordinatorはSecondViewControllerに対して作成されたCoordinatorで、SecondViewControllerへの遷移処理を実装したもの。作成方法については、FirstCoordinatorの作成方法がわかっていればできる!①については、FirstCoordinatorと異なる箇所で、Firstの場合はNavigationControllerのルートビューとして設定し、今回は、ルートビューからpushしていることに気をつける。

3. FireViewControllerにボタンを設置する。
4. ボタンをタップした際の処理を実装する。

Delegateを使用して、処理は、FirstCoordinatorに委任する。

FirstViewController.swift
// ①
protocol FirstViewControllerDelegate: AnyObject {
    func navigateToNextPage()
}

class FirstViewController: UIViewController {
    // ②
    weak var delegate: FirstViewControllerDelegate?

    // 省略

    @objc private func tapDetaiButton() {
        // ③
        delegate?.navigateToNextPage()
    }

}

①について、AnyObjectを継承している理由は、プロトコルが参照型だということを明示的にするため!(参考)②について、循環参照に対処するため、weakをつける!(参考)③について、メソッドの処理内容はFirstCoordinatorに委任される。

5. FirstCoordinatorFirstViewControllerDelegateを実装する。

FirstCoordinator.swift
extension FirstCoordinator: FirstViewControllerDelegate {
    func navigateToNextPage() {
        let secondCoordinator = SecondCoordinator(navigator: self.navigator)
        secondCoordinator.start()
        self.secondCoordinator = secondCoordinator
    }
}

Coordinatorを初期化後にstart()を実行することで、次のViewControllerへの遷移を行う。遷移先のCoordinatorをプロパティとして持つことで親子関係となる。

6. FirstViewControllerのdelegateプロパティにFirstCoordinatorを代入する。

FirstCoordinator.swift
final class FirstCoordinator: Coordinator {
    
    private let navigator: UINavigationController
    private var firstViewController: FirstViewController?
    private var secondCoordinator: SecondCoordinator?
    
    init(navigator: UINavigationController) {
        self.navigator = navigator
    }
    
    func start() {
        let vc = FirstViewController()
        // ①
        vc.delegate = self
        self.navigator.viewControllers = [vc]
        self.firstViewController = vc
    }
    
}

①について、delegateプロパティへの代入を忘れないこと!

7. ビルドして表示!

終わりに

基本的な内容を書いてみましたが、実際に使って試していきたいと思います!
ここまでご覧いただきありがとうございました!

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3