15. Jun 2023
iOSNáš iOS toolbox – Ako vytvárame reaktívne aplikácie v Swifte?
Reaktívne programovanie si v posledných rokoch získalo značnú obľubu a so zavedením frameworkov ako Combine sa stalo nevyhnutným nástrojom pre vývoj moderných iOS aplikácií. Keď sa však aplikácie stanú zložitejšími, riadenie reaktívneho programovania môže byť náročné. Tu prichádza balík GoodReactor, ktorý poskytuje jednoduchý, ale výkonný prístup k správe reaktívneho kódu.
Ako vytvoriť reaktívne aplikácie vo Swifte
GoodReactor používa architektúru založenú na protokole na vytvorenie reaktívneho komponentu, ktorý sa dá jednoducho znova použiť v celej aplikácii. Jeho protokolový prístup a integrácia s Combine z neho robia vynikajúci nástroj na správu komplexného, reaktívneho kódu.
Pomocou GoodReactor môžu vývojári písať čistý, udržiavateľný a testovateľný kód, ktorý je efektívny a ľahko spravovateľný.
Vytvorenie BaseViewController
Povedzme, že chceme vytvoriť jednoduchú aplikáciu počítadla pomocou balíka GoodReactor.
Ako prvé si vztvoríme BaseViewController
, ktorý bude slúžiť ako základná trieda pre ďalšie ViewControlleri v našej iOS aplikácii.
Použijeme generický typ T ako ViewModel, ktorý nám poskytne dáta a logiku pre náš ViewController. To znamená, že každá podtrieda nášho BaseViewController<T>
bude musieť mať svoj ViewModel pri inicializácií.
BaseViewController rovnako obsahuje parameter cancellables
. Ide o set objektov typu AnyCancellable
, ktorý budeme používať na sledovanie všetkých zmien publisherov v našom viewControlleri. Rovnako ich bude možné jednoducho zrušiť pri dealokácií ViewControllera.
class BaseViewController<T>: UIViewController {
let viewModel: T
var cancellables = Set<AnyCancellable>()
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
required init(viewModel: T) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
}
Vytvorenie ViewModel
Náš CounterViewModel
je príkladom toho, ako implementovať protokol GoodReactor
v Swift pomocou Combine frameworku.
import Combine
import GoodReactor
final class CounterViewModel: GoodReactor {
// State represents every state of data we want to use
struct State {
var counterValue: Int
}
// Action represents every user action
enum Action {
case increaseCounterValue
}
// Mutation represents state changes
enum Mutation {
case counterValueUpdated(Int)
}
internal let initialState: State
internal let coordinator: GoodCoordinator<AppStep>
init(coordinator: Coordinator<AppStep>) {
self.coordinator = coordinator
initialState = State(counterValue: 0)
}
}
Štruktúra State
predstavuje aktuálny stav zobrazenia. Stav obsahuje jeden parameter counterValue
, ktorá je inicializovaný s initialState
na 0.
Action
enum reprezentuje akcie používateľa. V tomto prípade sme definovali akciu increaseCounterValue
, ktorá sa spustí, keď používateľ klepne na tlačidlo na zvýšenie hodnoty počítadla.
Mutation
enum predstavuje zmeny stavu, ktoré sa spustia v reakcii na akcie používateľa. V tomto príklade je jedinou mutáciou counterValueUpdated
, ktorá aktualizuje hodnotu vlastnosti counterValue v state.
Nakoniec trieda tiež definuje metódu init
, ktorá preberá parameter Coordinator
, ktorý sa používa na inicializáciu vlastnosti coordinator
. Tento parameter sa používa na správu navigácie v appke a vyžaduje ju protokol GoodReactor
.
Teraz môžeme pokračovať v pridávaní potrebných metód v rámci nášho CounterViewModelu:
func navigate(action: Action) -> AppStep? {
return nil
}
func mutate(action: Action) -> AnyPublisher<Mutation, Never> {
switch action {
case .increaseCounterValue(let mode):
let increasedValue = currentState.counterValue + 1
return Just(.counterValueUpdated(increasedValue)).eraseToAnyPublisher()
default:
return Empty().eraseToAnyPublisher()
}
}
func reduce(state: State, mutation: Mutation) -> State {
var state = state
switch mutation {
case .counterValueUpdated(let newValue):
state.counterValue = newValue
}
return state
}
Metóda navigate
sa používa na ovládanie navigácie alebo prechodov obrazovky medzi rôznymi časťami aplikácie. Zavolaná akcia ViewModelu. Navigáciu má na starosti objekt GoodCoordinator
, ktorý je zodpovedný za riadenie toku medzi rôznymi časťami aplikácie. Typ AppStep
predstavuje konkrétny krok v rámci navigačnej hierarchie aplikácie. Viac informácií o koordinátoroch nájdete tu.
Metóda mutate
sa volá vždy, keď je prijatá akcia. V tomto prípade metóda vráti mutáciu counterValueUpdated
s novou hodnotou pre vlastnosť counterValue.
Metóda reduce
sa volá vždy, keď dôjde k mutácii. V tomto prípade metóda vráti nový stav s aktualizovaným parametrom counterValue.
Vytvorenie ViewController
Teraz definujeme náš CounterViewController
, ktorý bude zodpovedný za zobrazovanie a interakciu s údajmi poskytovanými inštanciou CounterViewModel
.
import UIKit
import Combine
final class CounterViewController: BaseViewController<CounterViewModel> {
// Label showing actual counter value
private let counterValueLabel = UILabel()
// Button responsible for increasing the counter value
private let increasingButton = UIButton()
override func viewDidLoad() {
super.viewDidLoad()
setupLayout()
bindState(reactor: viewModel)
bindActions()
}
}
V metóde viewDidLoad
sa volá metóda setupLayout
, ktorá nastaví rozloženie komponentov používateľského rozhrania vo ViewControlleri. Následne sa zavolajú metódy bindState
a bindActions
, ktoré sa používajú na vytvorenie obojsmernej komunikácie medzi ViewModelom a ViewControllerom.
Teraz pridajte nasledujúce funkcie do CounterViewController
:
// Sets up the layout of the UI components in the view
func setupLayout() {
[counterValueLabel, increasingButton].forEach { view.addSubview($0) }
NSLayoutConstraint.activate([
counterValueLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
counterValueLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
increasingButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
increasingButton.topAnchor.constraint(equalTo: counterValueLabel.bottomAnchor, constant: 16)
])
}
// binds viewModel state to UIComponents
func bindState(reactor: CounterViewModel) {
reactor.state
.map { String($0.counterValue) }
.removeDuplicates()
.assign(to: \\.text, on: counterValueLabel, ownership: .weak)
.store(in: &cancellables)
}
// bind user actions to the viewModel
func bindActions() {
increasingButton.addTarget(self, action: #selector(increasingButtonPressed(_ :)), for: .touchUpInside)
}
@objc func increasingButtonPressed(_ sender: UIButton) {
viewModel.send(event: .increaseCounterValue)
}
Metóda bindState
preberá vlastnosť viewModel
prvku viewController a spája ju s komponentmi používateľského rozhrania. To znamená, že kedykoľvek sa zmení stav CounterViewModel
, komponenty používateľského rozhrania sa zodpovedajúcim spôsobom aktualizujú.
Metóda bindState
prepojí zmeny Statu s jednotlivými UI prvkami v našom ViewControlleri. To znamená, že kedykoľvek sa zmení stav CounterViewModel
, komponenty používateľského rozhrania sa zodpovedajúcim spôsobom aktualizujú. Napr. v kóde vyššie bude counterValueLabel zobrazovať stav counterValue z ViewModelu.
Metóda bindActions
sa používa na prepojenie akcií používateľa na CounterViewModel
. To znamená, že vždy, keď používateľ interaguje s komponentmi používateľského rozhrania, zodpovedajúca akcia sa odošle do CounterViewModel
, ktorý následne aktualizuje svoj stav.
Gratulujeme!
Prešiel si procesom vytvárania reaktívnej aplikácie pomocou GoodReactor package.
Možno ťa teraz zaujíma, ako to funguje priamo v appke. Našťastie balík obsahuje ukážku, ktorá ti pomôže preskúmať a lepšie pochopiť funkčnosť balíka GoodReactor.
Ak bol tento balík užitočný, určite sa pozri aj na naše ďalšie balíky. Možno objavíš práve ten, ktorý ti pomôže posunúť tvoju appku na vyššiu úroveň!