
Koordinátor návrhového vzoru není nic nového. Existuje řada návodů, jak ho používat. Dnes vám ukážu, jak ho používáme my.
Proč:
Koordinátor návrhového vzoru pomáhá při rozdělování odpovědností v toku aplikace. Logika navigace tak přechází z view controllera na koordinátora a zároveň tím získáváme větší kontrolu nad navigačním tokem.
V čem jsou naši koordinátoři jiní?
Náš koordinátor je přizpůsobený pro práci s jednosměrným tokem podobným reduxu. Ukážu vám jednoduchý příklad koordinátora, jak ho děláme my, a na konci článku přidám odkaz na knihovnu, kde najdete i GoodCoordinator, který si můžete stáhnout do projektu přes SPM.
Začněme něčím jednoduchým. Nejdřív si vytvoříme soubor enum s názvem Coordinator.swift, který bude obsahovat enum s akcemi, které vypovídají o typu navigace. Měl by obsahovat základní akce, jako je push, present, dismiss, pop, a předvolenou hodnotu none.
import Combine import UIKit enum StepAction { case push(UIViewController) case present(UIViewController, UIModalPresentationStyle) case dismiss case pop case none }Následně si vytvoříme generickou třídu Coordinator. Tato třída bude sloužit jako vzor pro ostatní koordinátory, kteří po ní budou dědit. Bude obsahovat navigationController, který se bude vkládat v konstruktoru. Bude optional.
Následně potřebujeme step a cancellables. Jsou součástí knihovny combine dostupné od iOS 13.
Step bude mít property wrapper published, což znamená, že se k němu můžeme subscribenout a naslouchat změnám stejně, jako kdybychom použili didSet closure.
class Coordinator<Step> { weak var navigationController: UINavigationController? var cancellables: Set<AnyCancellable> = Set() @Published var step: Step? }Do třídy coordinator přidáme funkci navigate, která bude sloužit jako rozhraní pro child koordinátory, ale bude prázdná a nebude provádět žádnou funkci.
Následně definujeme privátní funkci navigate, která bude provádět standardní navigační akce, které jsme definovali nahoře nad navigation controllerem, který jsme uložili stejně, jako v třídě.
A nakonec definujeme public funkci start, která bude sloužit jako rozhraní child koordinátorů a zároveň bude při každém spuštění naslouchat změně proměnné step, jak už jsem uvedl výše. Step je optional, proto používáme compactMap, aby se zohlednily pouze nenulové hodnoty, a v sink closure voláme naši funkci private navigate.
@discardableResult func navigate(to stepper: Step) -> StepAction { return .none } private func navigate(flowAction: StepAction) { switch flowAction { case .dismiss: if let presentedViewController = navigationController?.presentedViewController { presentedViewController.dismiss(animated: true, completion: nil) } else { navigationController?.topViewController?.dismiss(animated: true, completion: nil) } case .push(let controller): navigationController?.pushViewController(controller, animated: true) case .pop: navigationController?.popViewController(animated: true) case .present(let controller, let style): controller.modalPresentationStyle = style if let presentedViewController = navigationController?.presentedViewController { presentedViewController.present(controller, animated: true, completion: nil) } else { navigationController?.topViewController?.present( controller, animated: true, completion: nil ) } case .none: break } } @discardableResult public func start() -> UIViewController? { $step .compactMap { $0 } .sink { [weak self] in guard let self = self else { return } self.navigate(flowAction: self.navigate(to: $0)) } .store(in: &cancellables) return navigationController }Teď nadefinujme konkrétní navigační akce. Vytvoříme si soubor AppStep.swift a napíšeme 2 jednoduché cases. Později je možné v tomto enume vrstvit strukturu na další koordinátory, přičemž každý case by reprezentoval jeden koordinátor a associated value jeho AppSteps. Teď pojďme definovat ten nejjednodušší případ.
enum AppStep { case showFirstViewController case showSecondViewController }Použijeme ho po úspěšném nastavení parent koordinátoru. Vytvoříme final class AppCoordinator, která bude dědit po Coordinator typu AppStep, který jsme si definovali níže.
V tomto koordinátoru nadefinujeme appwindow. Ve většině praktických případů se logika oken řeší právě tady, protože AppCoordinator obvykle rozhoduje o tom, zda se nacházíme v okně Login, nebo jsme už zalogovaní uvnitř apky. V našem případě použijeme pouze jedno okno, které bude automaticky zvolené od začátku.
Okno nastavíme v initu. A ve funkci start pro něj vytvoříme navigation controller. Okno zobrazíme.
import Foundation import UIKit import Combine // MARK: - Navigation & Initializers final class AppCoordinator: Coordinator<AppStep> { // MARK: - Properties private let appWindow: UIWindow // MARK: - Init override init() { appWindow = UIWindow() appWindow.frame = UIScreen.main.bounds } // MARK: - Overrides @discardableResult override func start() -> UIViewController? { super.start() let navigationController = UINavigationController() self.navigationController = navigationController appWindow.rootViewController = navigationController appWindow.makeKeyAndVisible() return navigationController } }Teď máme přípravu hotovou a pojďme do navigace něco pushnout. Overrideneme funkci a přes switch nad AppStepem vytvoříme rozhodovací logiku.
V každém AppStep jsme schopní pushnout viewController tak, že vrátíme AppStep s viewControllerem jako parametrem. Taky by se tady dala vrátit hodnota none a do navigation controlleru přidat další navigationController z jiného koordinátoru.
override func navigate(to step: AppStep) -> StepAction { switch step { case .showFirstViewController(let animated): let firstViewController = DocumentBrowserViewController(forOpeningFilesWithContentTypes: ["two", "one"]) return .push(firstViewController) case .showSecondViewController(let animated): let secondViewController = DocumentBrowserViewController(forOpeningFilesWithContentTypes: ["hone", "two"]) return .push(secondViewController) default: return .none } }Teď to zkusíme zapojit do apky. Ve třídě Appdelegate vytvoříme proměnnou appCoordinator a vytvoříme ho při zapínání aplikace. Koordinátor následně zavolá start a teď jen stačí nastavit step na potřebný AppStep.
import UIKit @main class AppDelegate: UIResponder, UIApplicationDelegate { private var appCoordinator: AppCoordinator! func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { let appCoordinator = AppCoordinator() self.appCoordinator = appCoordinator appCoordinator.start() appCoordinator.step = .showFirstViewController appCoordinator.step = .showSecondViewController return true } }Hotovo. Právě jsme použili Coordinator Pattern s použitím combinu. Koordinátor se dá velmi snadno škálovat a díky tomu je vhodný skoro všude.
Pojďme se podívat na situaci při spojení dvou koordinátorů. Vytvoříme si koordinátor AboutAppCoordinator.swift
// // AboutAppCoordinator.swift // CoordinatorArticle // // Created by Andrej Jasso on 27/09/2021. // import UIKit // MARK: - Steps final class AboutAppCoordinator: Coordinator<AppStep> { // MARK: - Overrides override func navigate(to step: AppStep) -> StepAction { switch step { default: return .none } } @discardableResult override func start() -> UIViewController? { super.start() let controller = UIViewController() let navigationController = UINavigationController() navigationController.viewControllers = [controller] controller.view.backgroundColor = .green return navigationController } }Tento koordinátor teď prezentujeme přes AppCoordinator step tak, že nahradíme obsah logiky druhého kroku.
case .showSecondViewController(let animated): let secondViewController = AboutAppCoordinator().start() guard let secondViewController = secondViewController else { return .none } return .present(secondViewController, .automatic)Hotovo. Proces koordinátorů máme úspěšně za sebou. Pomocí tohoto návodu byste měli být schopni vytvořit funkční navigaci projektu prostřednictvím koordinátorů.
Hotový sample najdete na mém GitHubu.
Knihovnu GoodIOSExtensions i s GoodCoordinatorem najdete na GitHubu GoodRequest.


