15. Jun 2023iOS

Náš 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.

Marek VricaniOS Developer

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ň!

Marek VricaniOS Developer