
App Intents: Jak zpřístupnit aplikaci pomocí Siri, Spotlight a Widgetů

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.


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.
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.






