
Návrhový vzor koordinátor nieje nič nové. Existuje mnoho návodov, ako ho možno použiť. Dnes ti ukážem, ako ho používame my.
Prečo:
Návrhový vzor koordinátor napomáha pri rozdeľovaní zodpovedností v toku aplikácie. Logika navigácie tak prechádza z view controllera na koordinátor a zároveň tým nadobudneme väčšiu kontrolu nad navigačným tokom.
Čím sú naše koordinátori iné?
Náš koordinátor je prispôsobený aby fungoval s jednosmerným tokom podobným reduxu. Ukážem ti jednoduchý príklad koordinátora, ako ho robíme my a na konci článku pridám odkaz na knižnicu ktorej súčasťou je GoodCoordinator, ktorý si môžeš stiahnuť do projektu cez SPM
Začneme niečím jednoduchým. Najskôr vytvoríme enum súbor s názvom Coordinator.swift, ktorý bude obsahovať enum s akciami, ktoré hovoria o type navigácie. Mala by obsahovať základné akcia ako push, present, dismiss, pop a predvolenú hodnotu none.
import Combine import UIKit enum StepAction { case push(UIViewController) case present(UIViewController, UIModalPresentationStyle) case dismiss case pop case none }Následne si vytvoríme generickú triedu Coordinator. Táto trieda bude slúžiť, ako vzor pre ostatné koordinátori, ktoré po ňom budú dediť. Bude obsahovať navigationController, ktorý sa bude vkladať v konštruktore. Bude optional.
Následne potrebujeme step a cancellables. Sú súčasťou combine knižnice dostupnej od iOS 13.
Step bude mať property wrapper published, čo znamená, že sa naň vieme subscribenuť a počúvať na zmeny podobne, ako keby sme použili didSet closure.
class Coordinator<Step> { weak var navigationController: UINavigationController? var cancellables: Set<AnyCancellable> = Set() @Published var step: Step? }Do triedy coordinator pridáme funkciu navigate, ktorá bude slúžiť, ako rozhranie pre child coordinatory ale bude prázdna a nebude vykonávať žiadnu funkciu.
Následne zadefinujeme privátnú funkciu navigate, ktorá bude vykonávať štandardné navigačné akcie, ktoré sme si zadefinovali hore nad navigation controllerom, ktorý sme si uložili rovnako, ako v triede.
A napokon si zadefinujeme public funkciu start, ktorá bude slúžiť ako rozhranie child koordinátorov a zároveň bude pri každom spustení počúvať na zmenu premennej step, ako už som vyššie spomenul. Keďže je step optional používame compactMap, aby boli brané do úvahy iba nenulové hodnoty a v sink closure voláme našu private navigate funkciu.
@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 }Teraz si poďme zadefinovať konkrétné navigačné akcie. Vytvoríme si súbor AppStep.swift a napíšeme 2 jednoduché case. Neskôr je možné v tomto enume vrstviť štruktúru na ďalšie koordinátori pričom, každý case by reprezentoval jeden koordinátor a associated value jeho AppSteps. Teraz poďme zadefinovať ten najjednoduchší prípad.
enum AppStep { case showFirstViewController case showSecondViewController }Po úspešnom nastavení parentKoordinátor ho poďme použit. Vytvoríme final class AppCoordinator, ktorá bude dediť po Coordinator typu AppStep, ktorý sme si nižšie zadefinovali.
V tomto koordinatori si zadefinujeme appwindow. Vo väčšine praktických prípadov sa logika okien rieši práve tu pretože AppCoordinator zvyčajne rozhoduje o tom, či sme na Login okne alebo už sme nalogovaný a vo vnútri appky. V našom prípade použijeme len jedno okno, ktoré bude automaticky zvolené od začiatku.
V inite okno nastavíme. A vo funkcii štart mu vytvoríme navigation controller. A 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 } }Teraz sme hotoví s prípravou a poďme do navigacie niečo pushnút. Overrideneme si funkciu a cez switch nad AppStepom vytvoríme rozhodovaciu logiku.
V každom appstep vieme pushnúť viewController tak, že vrátime appstep s viewcontrollerom ako parameter. Takisto by sa tu dalo vrátiť .none a do navigation controlleru pridať ďalší navigationController z iného coordiná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 } }Teraz to skúsme zapojiť do appky. V triede Appdelegate si vytvoríme premennú appCoordinator a vytvoríme ho pri zapínaní appky. Koordinátor následne zavoláme start a teraz stačí nastaviť step na AppStep, ktorý je potrebný.
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áve sme využili Coordinator Pattern s použitím combinu. Koordinátor sa dá veľmi ľahko škálovať a tak je vhodný skoro všade.
Poďme sa pozrieť, ako to vyzerá pri spájaní dvoch koordinátorov. Vytvorme 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 teraz prezentujeme cez 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. Úspešne sme prešli procesom koordinátorov. Pomocou tohto návodu by si mal vedieť vytvoriť funkčnú navigáciu projektu cez koordinátori.
Hotový sample nájdeš na mojom GitHube.
Knižnicu GoodIOSExtensions aj s GoodCoordinatorom najdete na GoodRequest GitHube.



