始めに
下記点において修正する方法はないか気になった。
-
push/present
をViewControllerで実装しない - 次のViewControllerを知らないようにする
調べてみたところ、Coordinator
パターンを見つけたので実装してみる。
Coordinatorパターンとは?
ChatGPTに聞いたらわかりやすかったので記録。
Coordinatorパターンとは、主にiOS開発で用いられるアーキテクチャのパターンの一つで、アプリケーションのナビゲーションフローを管理し、View Controllersが直接通信することを避けることを目的としています。Swift言語において、このパターンはコードの分離と再利用を容易にし、アプリケーションのテスト性を向上させるのに役立ちます。
Coordinatorパターンの基本的な概念は、各View Controllerが自身のナビゲーションロジックを持たず、その役割を専用のコーディネーターに委任することです。コーディネーターは、アプリケーション内で移動するためのナビゲーションロジックをカプセル化し、特定のView Controllerが他のView Controllerに依存しないようにします。
具体的な利点としては、以下のようなものがあります:
単一責任の原則:Coordinatorパターンを用いると、View ControllerはUIとユーザインタラクションの管理に専念し、ナビゲーションロジックはCoordinatorが担当します。これにより、コードの責任が分離され、より読みやすく、保守しやすいコードが生まれます。
再利用性:View Controllers間でナビゲーションロジックを共有する代わりに、それぞれのCoordinatorがそれぞれのナビゲーションフローを管理するため、特定のView Controllerを異なるコンテキストで再利用することが容易になります。
テスタビリティ:Coordinatorがナビゲーションロジックを担当することで、単体テストが容易になります。特定の入力に対して期待するナビゲーションアクションが行われるかをテストすることが可能となります。
SwiftのCoordinatorパターンは、効率的でスケーラブルなコードを書くのに役立つ強力なパターンです。しかし、その導入と実装はアプリケーションの規模と要件によります。適切な場合にはこのパターンを活用することで、アプリケーションの設計と保守性を大幅に改善することができます。
Coordinatorパターンの詳細については、下記を参考にする。
TabBarControllerをrootViewControllerに設定している例が載っている。とてもわかりやすい!
下記のわかりやすかった!
実際にコードを書いていく
実践1
NavigationController
を使用して、FirstVC
を表示する。
protocol Coordinator {
func start()
}
Coordinator
プロトコルを定義することで、Coordinatorを適用する全てのクラスが、start
メソッドを持つことが保証され、一貫した操作が可能になる。
2. 起動時に表示するFirstViewController
を作成する。
3. FirstViewControllerと紐づく、FirstCoordinator
を作成する。
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が存在する。そして、画面遷移の経路に沿った親子関係を構築する。
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にて、初期画面の設定をする。
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を作成する。
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
に委任する。
// ①
protocol FirstViewControllerDelegate: AnyObject {
func navigateToNextPage()
}
class FirstViewController: UIViewController {
// ②
weak var delegate: FirstViewControllerDelegate?
// 省略
@objc private func tapDetaiButton() {
// ③
delegate?.navigateToNextPage()
}
}
①について、AnyObjectを継承している理由は、プロトコルが参照型だということを明示的にするため!(参考)②について、循環参照に対処するため、weak
をつける!(参考)③について、メソッドの処理内容はFirstCoordinator
に委任される。
5. FirstCoordinator
でFirstViewControllerDelegate
を実装する。
extension FirstCoordinator: FirstViewControllerDelegate {
func navigateToNextPage() {
let secondCoordinator = SecondCoordinator(navigator: self.navigator)
secondCoordinator.start()
self.secondCoordinator = secondCoordinator
}
}
Coordinatorを初期化後にstart()を実行することで、次のViewControllerへの遷移を行う。遷移先のCoordinatorをプロパティとして持つことで親子関係となる。
6. FirstViewControllerのdelegateプロパティにFirstCoordinatorを代入する。
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. ビルドして表示!
終わりに
基本的な内容を書いてみましたが、実際に使って試していきたいと思います!
ここまでご覧いただきありがとうございました!