W kilku poprzednich artykułach (tutaj, tutaj i tutaj) starałem się przybliżyć Wam programowanie raktywne na platformie iOS. W tym celu korzystaliśmy z biblioteki RxSwift, która pozwala na pisanie kodu w sposób deklaratywny (określamy co chcemy osiągnąć, ale nie mówimy jak). Istnieje jeszcze jedna bardzo przydatna biblioteka, która dostarcza nam „reaktywnych wraperów” dla obiektów wchodzących w skład frameworka Cococa. Dzięki niej mamy możliwość pracować na przykład z elementami UI zgodnie z paradygmatem reaktywnym. RxSwift został celowo oddzielny od RxCocoa, ponieważ autorzy chcieli całkowicie odseparować implementacje funkcjonalności przeznaczonych jedynie dla platform iOS oraz macOS.

Mała uwaga. Jeżeli do tej pory nie przeczytaliście podlinkowanych wyżej artykułów, to teraz jest najlepszy moment. Informacje w nich zawarte będą Wam potrzebne do pełnego zrozumienia dalszej części artykułu. Bez tego niestety utrudnicie sobie znacznie zadanie.

 

Na potrzeby tego krótkiego tutotrialu napiszemy wspólnie prostą aplikację, która będzie wyszukiwała podane przez nas frazy w wyszukiwarce Wikipedii. Trik będzie polegał na tym, że wszystkie operacje wykonamy zgodnie z podejściem reaktywnym. Właśnie w tym celu będzie nam potrzebny duet RxSwift + RxCocoa. Link do projektu startowego możecie znaleźć poniżej:

 

Link – projekt startowy

 

Aplikacja będzie miała tylko jedno proste zadanie. Użytkownik będzie wpisywał w polu tekstowym interesujące go zagadnienie, które następnie zostanie wyszukane w Wikipedii. Jako wynik zostanie zwrócona liczba wyszukań danego wyrażenia. Wyszukiwanie będzie się odbywało na angielskiej wersji platformy, dlatego polskich wyrazów mija się z cele 😆.

Projekt startowy składa się z zaledwie kilku obiektów, a wszystkie operacje będziemy wykonywali w pliku WikiiSearchViewController.swift. Dla całkowitego uproszczenia przykładu projekt nie będzie korzystał z żadnej architektury. Jeżeli chcecie jednak zobaczyć jak można wykorzystać RxSwift / RxCococa to zapraszam do lektury artykułu „MVVM w praktyce”.

Otwórzcie sobie teraz projekt za pomocą pliku WikiiSearchRxCocoa.xcodeproj. Wszystkie outlety dla elementów UI zostały już połączone. Skompilujcie aplikację i uruchomcie ją na dowolnym emulatorze, aby sprawdzić czy wszystko działa jak należy. Waszym oczom powinno ukazać się proste UI:

 

 

W polu UITextField będziemy wpisywali wyszukiwaną frazę, natomiast UILabel będzie wyświetlało dla nas wynik. Z widocznego Activity Indicator będziemy korzystali w końcowej części wpisu.

Otwórzcie sobie teraz okno nawigacji projektu. W pliku WebService.swift został umieszczony kod potrzebnym do przeszukiwania Wikipedii. Został on napisany zgodnie z podejściem reaktywnym. Nie będę wnikał tutaj w jego szczegóły ponieważ podobny kod został wykorzystany w artykule „Jak napisać aplikację z RxSwift”. WikiResponse.swift zawiera natomiast prosty model, który posłuży do przechowywania wyniku poszczególnych wyszukań.

Otwórzcie teraz plik podfile. Dodałem już za Was poszczególne zależności, ale instalacja będziecie musi wykonać sami:

RxSwift wyjaśniać już na tym etapie nie muszę. SwiftyJSON też nie jest szczególnie istotny ponieważ posłuży nam jedynie do łatwiejszego parsowanie odpowiedzi z serwera.

Będzie nas natomiast interesowała biblioteka RxCococa, tutaj w wersji 4.0. RxSwift oraz RxCocoa mają ten sam cykl wydawniczy, dlatego dobrym pomysłem będzie pobieranie tej samej wersji dla każdej z bibliotek.

Możecie teraz zamknąć projekt i otworzyć jego lokalizację w terminalu. Następnie wykonajcie standardowe polecenie pod install, aby zainstalować wymagane zależności. Po zakończonej instalacji otwórzcie projekt za pomocą pliku .xcworkspace i zajrzyjcie do nowo dodanych podsów. W folderze RxCocoa możecie znaleźć wszystkie „wrapery”, które będą do Waszej dyspozycji. W naszym przykładowym projekcie będziemy korzystali z obiektów UITextFiled oraz UILabel, dlatego warto sprawdzić jak prezentują się dedykowane im rozszerzenia. Znajdziecie je w plikach UITextFiled+Rx.swift oraz UILabel+Rx.swift.

Rozszerzenie UITextFiled zawiera pole o nazwie text, które dziedziczy po ControlProperty. Jest to specjalny typ Subject (obiekty bazowe to ObservableType oraz ObserverType) przystosowany do pracy elementami UI. Jak to z obiektami typu Subject bywa, może on jednocześnie emitować, jak i przyjmować nowe wartości umieszczane w nim manualnie.

Rozszerzenie UILabel zawiera natomiast dwa pola – text oraz attributedText – które korzystają z typu Binder. Binder jest specjalnym typem Observera, który podobnie do ControlProperty został przystosowany do pracy z UI. Nie może on wiązać errorów, a obserwacja domyślne wykonywana jest wątku głównym.

 

Przeszukiwanie Wikipedii

 

Możemy teraz przejść do części najważniejszej, czyli implementacji wyszukiwania poszczególnych fraz w Wikipedii. Cały kod dla naszej aplikacji będziemy umieszczali w pliku WikiiSearchViewController.swift, w metodzie viewDidLoad(). Jest to najlepsze miejsce do zapisania się na obserwację danego strumienia, ponieważ wywołanie viewDidLoad() oznacza, że widok został już w całości załadowany do pamięci, ale poszczególne elementy UI nie zostały jeszcze wyświetlone. Dzięki temu mamy pewność, że nie ominą ich żadne zdarzenia. Subskrypcja musi zostać wykonana przed operacją, która może potencjalnie wpłynąć na stan interfejsu.

Na samym początku funkcji viewDidLoad() dodajcie poniższy kod:

W pierwszej kolejności dodajemy naszego observable do zmiennej, dzięki czemu będziemy mogli korzystać z niego wielokrotnie. Jest to bardzo przydatna technika, o czym się za chwilę przekonacie 😆. Korzystamy tutaj z pola text, które zostało umieszczone w rozszerzeniu RxCocoa.

Za pomocą funkcji filter odrzucamy wszystkie przypadki, w których pole pozostaje puste. W następnym korku, korzystając z funkcji flatMapLatest, przekazujemy wartość pola text do funkcji getSuccesResponseWiki(searchString:), która odpowiedzialna będzie za pobranie danych z serwera. Wewnątrz bloku, w którym został umieszczony kod do pobierania danych z serwera, umieszczamy unowned self. Dzięki temu unikniemy zjawiska strong reference cycle. Na tym etapie pewnie wiecie już z czym to się wiąże, ale jeżeli z jakiegoś powodu Wam to umknęło to zapraszam do tego artykułu.

Waszą uwagę zwróciła pewnie funkcja catchErrorJustReturn(). Można ją traktować jako standardowe zabezpieczenie przed ewentualnym błędem wywołanym podczas wykonywania operacji w funkcji getSuccesResponseWiki(searchString:). W razie jakiegokolwiek potknięcia zwrócona zostanie wartość domyślna, czyli w tym przypadku obiekt WikiResponse z wartością pola totalHits ustawioną na 0. Bez tego zabezpieczenia w przypadku wystąpienia dowolnego błędu, strumień danych zostałby przerwany. W przypadku pracy z UI takie zachowanie jest niewskazane, o czym więcej będę pisał poniżej, przy okazji opisywania Binderów.

Ostanim krokiem będzie określenie na jakim wątku ma nastąpić obserwacja wyniku. Będziemy go przekazywali do elementów UI, więc musimy wrócić na wątek główny aplikacji (pobieranie samych danych będzie się odbywało na wątku drugorzędnym).

Bez wykorzystania możliwości RxCococa dalsza praca z utworzonym powyżej observable wyglądałaby mniej więcej tak (nie wstawiajcie poniższego kodu do aplikacji):

Ale jest na to lepszy sposób 😎.

 

 

Wykorzystanie funkcji bind(to:)

 

Bind jest specjalną funkcją, której celem jest lepsze „wyrażenie intencji” programisty. Bind jest w stanie powiązać dany observable z obiektem, który spełnia wymogi protokołu ObserverType. Pracując z elementami UI musimy pamiętać, że subskrypcja z wykorzystaniem bind(to:) musi zostać wykonana na wątku głównym. W przeciwnym wypadku zwrócony zostanie błąd „Element can be bound to user interface only on MainThread.”.

Tak to będzie wyglądało w praktyce. Dodajcie poniższy kod na samym końcu funkcji viewDidLoad():

Prawda, że krócej? Za pomocą funkcji bind, „wiążemy” observable search do pola text, które dziedziczy po obiekcie Binder. Ale można to zrobić jeszcze lepiej 🤓. Na razie jednak skompilujcie aplikację, aby sprawdzić, czy wszystko działa jak należy. Na tym etapie możecie już wpisać wyszukiwaną frazę w pole tekstowe. Wynik w postaci liczby trafionych wyszukań zostanie wyświetlony tuż pod wskaźnikiem aktywności. Aplikacja nie sprawdza połączenia z internetem, dlatego upewnijcie się, że macie dostęp do sieci zanim przystąpicie do testowania.

 

 

 

Traits

 

Traits są opcjonalną implementacją, która stanowi tylko pewne ułatwienie. Zostały one zaprojektowane dla ułatwienia pracy z elementami UI w aplikacji. Ich najważniejsze cechy to:

  • Dzielą „efekty uboczne” (side effects) – oznacza to po prostu, że mogą one dzielić się wynikami, jak również zasobami wykorzystanymi przy wykonywaniu obliczeń.
  • Aą obserwowane na wątku głównym (poprzez Main Scheduler) – nie musimy definiować tego ręcznie.
  • Nie mogą zostać przerwane przez błąd – sekwencja dostarczająca dane dla UI nie może zostać przerwana, dlatego zawsze jakaś wartość będzie dostępna (więcej za chwilę).

Celem Traits jest dostarczenie intuicyjnego sposobu pracy z warstwą UI aplikacji (luźny cytat z dokumentacji 🤨). W przypadku RxCocoa w skład Traits wchodzą:

Driver jest specjalną wersją Observable, który wszystkie operacje wykonuje na wątku głównym, dzięki czemu mamy pewność, że wszelkie zmiany w UI nie zostaną przypadkiem wykonane na jakimś wątku typu background. Oczywiście nie może on zostać przerwany przez błąd. Jeżeli zostanie przerwany przez jakiś error to zwróci domyślną wartość zdefiniowaną przez użytkownika. Driver domyślne dzieli się ostatnią wyemitowaną wartością za pomocą (share(replay: 1, scope: .whileConnected)).

Signal jest bardzo podobny do Drivera, z tą różnicą, że w przypadku nowej subskrypcji nie powtarza ostatniego elementu. W dalszym ciągu dzielni on jednak zasoby pomiędzy wszystkimi „subskrybentami”. Oznacza to, że na przykład przy pobieraniu danych z serwera potrzebne zasoby (obiekty) zostaną umieszczone w pamięci tylko raz i będą wykorzystywane przez wszystkich subskrybentów.

Control Property poznaliście przed chwilą. Control Event jest natomiast kolejnym przydatnym rozszerzeniem udostępnionym przez RxCocoa. Control Event nasłuchuje, czy został wywołany jakiś event powiązany z dany elementem UI. W przypadku UITextFiled, będzie to na przykład naciśnięci przycisku „Return”, a w przypadku UIButton interakcja użytkownika z samym obiektem.

Skoro wiemy już dlaczego Traits są tak przydatne, to możemy zaimplementować je w naszej aplikacji. W pierwszym kroku musimy przekształcić naszego obervable do pobierana danych na Drivera. Podmieńcie implementację dla zmiennej search:

Teraz kompilator zgłosi nam błąd przy funkcji bind(to:), ponieważ nie została ona przewidziana do pracy z Driverami. Wystarczy jednak dokonać małej zmiany:

Funkcja drive działa niemal identycznie jak bind(to:), ale jej nazwa lepiej wskazuje na przeznaczenie tworzonej w ten sposób subskrypcji. Skompilujcie sobie teraz projekt, aby sprawdzić, czy wszystko dalej działa jak należy. W działaniu samej aplikacji nie powinniście zauważyć żadnej różnicy.

Możemy jednka jeszcze bardziej zoptymalizować kod w naszej aplikacji. W tym celu skorzystamy z ControlEvent, które to udostępniane jest dla UITextFiled. Nie ma większego sensu wysyłanie żądania do serwera za każdym razem kiedy użytkownik wpisze jakąś literę w pole tekstowe. Bardziej optymalnie będzie poczekać, aż użytkownik naciśnie przycisk „Search” na klawiaturze.

Definicję całej zmiennej search zamieńcie na poniższy kod:

Korzystając z ControlEvent zapisujemy się do nasłuchiwania zdarzenia .editingDidEndExit, które zostanie wywołane w momencie naciśnięcia przez użytkownika przycisku „Search” na klawiaturze urządzenia. Następnie obiekt ControlEvent przekształcamy w Observable, a korzystając z funkcji map przekształcamy emitowany event w wartość pola text naszego pola tekstowego.

Dalej mamy już standardową operację filtrowania, pobierania danych z serwera, a na końcu przekształcania observable w drivera. Jeszcze raz skompilujcie aplikację. Tym razem wyszukiwanie danego wyrażenia w Wikipedii powinno rozpocząć się dopiero po naciśnięci przycisku „Search” (lub enter jeżeli korzystamy z emulatora).

Dodamy jeszcze tylko mały bonus do naszej aplikacji i będziemy mogli uznać ją za skończoną 😎. RxCocoa daje nam także dostęp do innych powszechnie używanych właściwości elementów UI. Do tej pory na ekranie naszej aplikacji cały czas wyświetlany był activity indicator, ale jakoś specjalnie nie chciał z nami współpracować. Zmienimy to za pomocą pola isAnimating, dostępnego w rozszerzeniu rx. Będzie nam potrzebny osobny observer do emitowania zdarzeń informujących o rozpoczęciu nowego wyszukiwania w Wikipedii. Dodajcie poniższy kod na samym początku funkcji viewDidLoad():

Za chwilę zrobimy użytek z nowej zmiennej. Najpierw jednak zrobimy mały refaktoring zmiennej search. Podmieńcie całą jej implementacje na poniższy kod:

Korzystamy tutaj z searchEvent, aby się nie powtarzać.

Teraz połączymy searchEvent oraz search, aby rozkręcić activity indicator. Dodajcie poniższy kod na samym końcu funkcji viewDidLoad():

W zmiennej isAnimating będziemy przechowywać połączone searchEvent oraz search. Zasada działania jest prosta. W momencie wyemitowania przez searchEvent zdarzenia, będziemy przekształcać je na wartość boolean równą true. Będzie to oznaczało, że rozpoczęliśmy wyszukiwanie, a więc activity indicator powinien zacząć „się kręcić”. Z kolei wyemitowanie zdarzenia przez observable search będzie mapowane do wartości false – sygnał, dla activity indicator, że powinien się zatrzymać.

I to wszystko. Uruchomcie aplikacje, aby sprawdzić kod w działaniu. Udało nam się tchnąć troche życia w niemrawy do tej pory „wskaźnik aktywności” 😆.

 

Link do końcowego projektu

 

Słowo na drogę

 

Jak sami mogliście się przekonać, zrozumienie podstaw RxCococa nie jest specjalnie trudne, ale właściwe wykorzystanie zdobytej wiedzy wymaga odrobiny praktyki. Konstrukcje takie takie jak Traits pomagają z jednej strony w pisaniu bardziej zwięzłego kodu, ale z drugiej potrafią „zaciemnić” i tak już złożoną strukturę RxSwift’a. Niemniej, korzystanie z RxCococa jest właściwie obligatoryjne jeżeli chcemy aplikacje pisane na iOS’a w całości oprzeć o „reaktywny” kod. Jak to ze wszystkim w nauce programowania bywa, z czasem taki styl pisania stanie się dla Was zupełnie naturalny. Do następnego 🧐.

 

Link do grafiki tytułowej

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *