30. Nov 2021iOS

Ako používa Coordinator Pattern v Swift náš iOS tím?

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.

Andrej JaššoiOs Developer

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.

Andrej JaššoiOs Developer