Jakiś czas temu skleciłem całkiem długi artykuł, który stanowi wprowadzenie do zagadnień związanych z programowaniem reaktywnym. Idąc za ciosem postanowiłem pokazać Wam także stronę praktyczną. W tym celu stworzyłem poradnik, w którym krok po korku opiszę  jak programowanie reaktywne może przydać się w codziennej pracy. Trochę tego przygotowałem 😆. Przed przystąpieniem do kodowania, zachęcam jednak mocno to przeczytania podlinkowanego powyżej artykułu, bo podstawowa wiedza teoretyczna się przyda.

 

 

Korzystając z tego poradnika stworzycie aplikację, która będzie pobierała aktualne notowania kryptowalut z API Coin Market Cap. Tym razem jednak główne funkcjonalności aplikacji będą zaimplementowane z wykorzystaniem RxSwift. Po zakończeniu aplikacja będzie prezentowała się w następujący sposób:

 

 

Nic specjalnego, ale całe piękno kryje się we wnętrzu 😎.

Projekt startowy możecie pobrać za pomocą poniższego linka:

 

Projekt startowy CoinsCap

 

Jeżeli utkniecie na jakimkolwiek etapie to możecie sprawdzić ukończony projekt na moim GitHubie:

 

CoinsCap na GitHub

 

Przygotowanie projektu

 

Dla ułatwienia w projekcie zainstalowałem już wymagane biblioteki, ale otwórzycie sobie plik podfile żeby sprawdzić co się w nim znajduje:

Biblioteki RxSwift wyjaśniać nie muszę. SwiftyJSON będzie nam potrzebne na etapie mapowania danych z serwera do obiektów reprezentujących nasze modele. Nie musicie specjalnie przejmować się tą biblioteką, bo użyjemy jej tylko w jednym miejscu. Umieściłem ją tutaj, ponieważ sam korzystam ze SwiftyJSON na co dzień i chciałem Wam pokazać jak przydatna potrafi być ta biblioteka.

Krótkiego wyjaśnienia wymaga jednak druga pozycja. RxCocoa została oparta na RxSwift i stanowi zbiór bardzo przydatnych API, które dodają „reaktywne” rozszerzenia do najbardziej popularnych narzędzi wchodzących w skład podstawowych bibliotek Apple’a – takich URLSession, czy też UITableView. Omówienie RxCocoa wymaga osobnego wpisu, ale w naszej aplikacji wykorzystamy ją tylko dla rozszerzenia możliwości obiektu URLSession.

Możecie teraz otworzyć projekt wykorzystując do tego plik CoinsCap_RxSwift.xcworkspace. Skompilujcie aplikację i uruchomcie ją na emulatorze lub urządzeniu fizycznym, aby sprawdzić, czy wszystko działa jak należy. Waszym oczom powinien ukazać się poniższy obrazek:

 

 

W tej chwili nie dzieje się w aplikacji nic ciekawego, ale za chwilę to zmienimy 😏. Możecie rozejrzeć się po strukturze projektu. Najważniejsze pliki zostały umieszczone w folderze CoinCap. Znajdują się w nim dwa obiekty ViewController oraz struktura zawierająca model danych.

 

Połączenie ze zdalnym api

 

Zajmiemy się teraz implementacją najważniejszej funkcjonalności w aplikacji, czyli pobieraniem danych z serwera.

Otwórzcie plik MainViewVC.swift. Funkcja getCurrentCoinsCap(fromUrl:) nie zawiera w tej chwili żadnej implementacji i to właśnie w niej umieścimy za chwilę kod potrzebny do pobrania aktualnych cen kryptowalut. W tym celu wykorzystamy URLSession, a więc podstawowa wiedza na temat tego API będzie przydatna. Nie będziemy jednak korzystali z domyślnej implementacji – wykorzystamy rozszerzenie, które zostało umieszczone w bibliotece RxCococa. Pozwoli nam to na pisanie kodu zgodnie z podejściem reaktywnym. Za chwilę zobaczycie jak to się sprawdza w praktyce.

Funkcja getCurrentCoinsCap(fromURL:) jako argument przyjmuje obiekt String, który posłuży nam do zbudowania adresu URL skrywającego aktualne notowania kryptowalut. Zacznijcie od dodania wewnątrz funkcji następującego fragmentu kodu:

Za jego pomocą utworzycie obiekt Observable składającego się tylko z jednego elementu (stąd nazwa funkcji – ‚just’), który będzie emitowany w momencie utworzenia subskrypcji. To punkt startowy do utworzenia pełnego adresu URL.

Teraz za pomocą funkcji map wchodzącej w skład biblioteki RxSwift utworzymy właściwy obiekt URL:

Mała uwaga odnośnie funkcji map. Nie jest to ta sama funkcja, która wchodzi w skład standardowej biblioteki Swift (tej używanej na kolekcjach). Zasada jej działania jest identyczna, jednak warto wiedzieć, że pochodzi ona z biblioteki RxSwift.

W powyższym przykładzie idziemy trochę na skróty używając force unwrap do utworzenia adresu, ale robimy to tylko dla zachowania przejrzystości przykładu. W codziennej pracy koniecznie upewnijcie się, że kod nie wygeneruje Wam błędu.

Podawanie typu danych zwracanych przez blok nie jest konieczne, ale bez nich kompilator może czasami się trochę pogubić, więc dla bezpieczeństwa lepiej je dodawać.

Idziemy dalej. Mamy już adres URL, więc możemy przystąpić do utworzenia obiektu, za pomocą którego będziemy mogli przesłać nasze żądanie do serwera:

Ponownie korzystamy z funkcji map, dzięki której w łatwy sposób przekształcimy strumień danych (w naszym wypadku jest to tylko jeden obiekt URL) w dowolną, wymaganą formę.

Możemy teraz wysłać nasze żądanie do serwera. Tym razem skorzystamy z flatMap (również z biblioteki RxSwift). Dodajcie do łańcucha poleceń następujący kod:

Tutaj musimy wyjaśnić sobie specjalną właściwość flatMap. Funkcja ta oprócz łączenia poszczególnych strumieni danych potrafi wstrzymać wykonanie łańcucha instrukcji do chwili zakończenia wykonywania asynchronicznego kodu. Tak właśnie dzieje się w przypadku powyższego przykładu. Pobieranie danych z serwera za pomocą URLSeesion jest wykonywane asynchronicznie, a flatMap spokojnie czeka na odpowiedź z serwera. Dopiero w chwili odebrania danych zostaną wykonane kolejne operacje, które za chwilę dodamy do łańcucha.

Waszą uwagę zwrócił pewnie specyficzny zapis operacji wykonywanych za pomocą obiektu URLSession. Funkcja response(request:) jest częścią biblioteki RxCocoa, która jak już wspomniałem dostarcza nam rozszerzeń funkcjonalności bibliotek Apple.

Obiektem zwracanym przez response(request:) jest Observable, który emituje z kolei dwa typy danych HTTPURLResponse oraz Data. Obiekty te posłużą nam za chwilę do filtrowania odpowiedzi z serwera. Observable emituje dane tylko raz, a następnie wysyła event onCompleted(), kończąc w ten sposób swoje działanie. Zwróćcie uwagę na fakt, że do obsługi żądania wysłanego do serwera wystarczyło nam tylko kilka linijek kodu.

 

 

Można się trochę pogubić, więc podsumujmy to co do tej pory zrobiliśmy:

  1. Wewnątrz funkcji getCurrentCoinsCap(fromURL:) na samy początku utworzyliśmy obiekt Obervable, który będzie emitował tylko jeden obiekt typu String.
  2. Przy pomocy funkcji map i przekazanego parametru utworzyliśmy obiekt URL.
  3. W kolejnym kroku ponownie wykorzystaliśmy funkcję map do  utworzenia obiektu URLRequest, korzystając tym razem z obiektu URL z poprzedniego kroku.
  4. W przedostatnim korku wewnątrz funkcji flatMap skorzystaliśmy z obiektu URLRequest oraz rozszerzenia obiektu URLSession, aby wysłać zapytanie do serwera oraz poczekać na odpowiedź.

 

 

Filtrowanie operacji zakończonych sukcesem

 

Na tym etapie mamy gotowy kod do pobierania danych z serwera, ale będziemy musieli jeszcze te dane odpowiednio przygotować zanim skorzystamy z nich w aplikacji. Musimy przerobić je na odpowiednie obiekty, a dokładnie rzecz ujmując na kolekcję obiektów Coin. Odpowiednia struktura dla modelu znajduje się w pliku Coin.swift. Dla ułatwienia skorzystamy z biblioteki SwiftyJSON, która została już umieszczona w projekcie. Za jej pomocą będziemy dokonywać deserializacji danych przychodzących z serwera pod postacią obiektów JSON.

Zaczniemy od filtrowania odpowiedzi, które będą informowały o powodzeniu operacji, a tym samy będą zwierały interesujące nas dane. Dodajecie w dowolnym miejscu klasy MainViewVC pustą funkcję filterSuccesResponse():

We wnętrzu funkcji będziemy wykonywać wszystkie operacje związane z przetwarzaniem odpowiedzi z serwera. Jako argument przekażemy obiekt Observable, który został utworzony w funkcji getCurrentCoinsCap(fromURL:).

Dodajcie teraz do funkcji następujący kod:

Funkcja observeOn() pozwala nam określić na jakim wątku chcemy, aby wykonywana była „obserwacja”. Operacja pobierania danych z serwera jest wykonywana na wątku ‚w tle’ (background thread), ale zaktualizowanie widoku tabeli będzie wymagało przeniesienia się na wątek UI (do tego etapu za chwilę dojdziemy). Można oczywiście skorzystać ze wsparcia Grand Central Dispatch, ale lepiej będzie zaimplementować rozwiązanie oferowane przez RxSwift.

Jako parametr do funkcji observeOn() przekazujemy instancję obiektu MainScheduler, który działa na wątku głównym aplikacji. Warto tutaj zaznaczyć, że RxSwift nie tworzy nowych wątków w aplikacji, a jedynie korzysta z tego co udostępnia nam Apple. W grę wchodzą DispatchQueues (GDC) lub NSOperationQueue (NSOperation). Temat wielowątkowości w RxSwift jest hmm… wielowątkowy 🤣. Nie będę się tutaj o nim rozpisywał, bo nie chcę komplikować prostego przykładu. W przyszłości na pewno poświęcę mu jednak osobny artykuł.

Za pomocą funkcji filter przepuszczamy dalej jedynie te odpowiedzi, które zawierają stausCode potwierdzający powodzenie operacji. Filtrowaniem kodów informujących o błędzie zajmiemy się w dalszej części artykułu.

W następnym korku przekształcimy dane z serwera w obiekty JSON (z biblioteki SwiftyJSON). Dodajcie do łańcucha poniższy kod:

Za pomocą funkcji map tworzymy kolekcję składającą się z obiektów JSON. Tutaj niestety łatwo o pomyłkę. W tym przypadku mamy na myśli obiekty JSON pochodzące ze SwiftyJSON, a nie „surowe” obiekty json pochodzące z serwera.

W bloku „do” sprawdzamy najpierw, czy dane z serwera można odpowiednio przekształcić. W przypadku powodzenia zwracamy nowy obiekt JSON. W przeciwnym razie wyświetlamy błąd w konsoli (możecie w tym miejscu wyświetlić okienko z błędem) oraz zwracamy pusty obiekt JSON. Ja właśnie tak lubię radzić sobie z błędami. Zamiast wyświetlać trudne do zrozumienia komunikaty, wolę wyświetlić na dole ekranu subtelne okienko popup informujące o nieprawidłowościach, a następnie zaprezentować pustą listę.

Idziemy dalej. Dodajcie do łańcucha poniższy kod:

W tym przypadku funkcja filter pozwala nam odsiać kolekcje, które są puste i przepuścić dalej tylko te, które zawierają jakieś elementy.

Możemy w końcu przekształcić dane na odpowiednie obiekty. Dodajcie poniższy kod:

Ponownie wykorzystujemy funkcję map do utworzenia kolekcji obiektów, tym razem jednak w jej skład będą wchodziły obiekty Coin. W pierwszej kolejności upewniamy się, że kolekcja, którą otrzymaliśmy w poprzednim kroku może być traktowana jako standardowa tablica. Jeżeli wynik jest pozytywny to w następnym kroku wykorzystujemy funkcję map ze standardowej biblioteki Swift (to nie jest ta sama funkcja, która występuje w RxSwift), aby kolejno przekształcić każdy obiekt JSON w obiekt typu Coin. W przypadku wyniku negatywnego zwracam pustą kolekcję, podobnie jak w operacji wykonywanej dwa kroki wcześniej.

Teraz wielki finał, czyli utworzenie właściwej subskrypcji, za pomocą której będziemy nasłuchiwać emitowanych danych. Dodajcie poniższy kod do łańcucha:

Subskrypcja będzie otrzymywała kolekcję obiektów typu Coin, które zostały utworzone w wyniku działania poszczególnych operacji w łańcuch. Kolekcja posłuży ostatecznie do zaktualizowania interfejsu użytkownika. Na samym końcu subskrypcję dodajemy do obiektu DisposeBag, który pozbędzie się jej wraz z usunięciem aktualnego obiektu ViewController z pamięci. W tym konkretnym przypadku operacja ta zostanie wykonana dopiero w momencie zamknięcia aplikacji, ponieważ MainViewVC jest głównym kontrolerem aplikacji.

Implementacja filterSuccesResponse() jest gotowa, dlatego możecie teraz dodać jej wywołanie na samym końcu funkcji getCoinsFromURL():

 

 

Filtrowanie operacji zakończonych niepowodzeniem

 

Mamy już kod do przetwarzania operacji zakończonych powodzeniem, ale musimy jeszcze poradzić sobie z przypadkami, w których coś poszło nie tak. Zaraza pod funkcją filterSuccesResponse() dodajcie następujący kod:

W funkcji filterErrorResponse() będziemy przetwarzać operacje, które z jakiegoś powodu zakończyły się niepowodzeniem. Zacząć musimy od przechwycenia tylko tych odpowiedzi z serwera, które zawierają kod błędy 4xx (błąd po stronie klienta) lub 5xx (błąd po stronie serwera). Dodajcie wewnątrz funkcji poniższy kod:

Funkcja observeOn() ma takie same zastosowanie jak w przypadku filtrowania operacji zakończonych sukcesem.

Za pomocą funkcji filter przepuszczamy dalej jedynie te odpowiedzi z serwera, które zawierają w sobie kody odpowiedzi HTTP pomiędzy 400 a 600, czyli wszystkie kody 4xx oraz 5xx. Drugi parametr w bloku funkcji filter (czyli obiekt Data) możemy pominąć, bo nie będzie nam on do niczego potrzebny.

Następnym krokiem będzie wyemitowanie kodu odpowiedzi za pomocą obiektu Observable. Dodajcie poniższy kod do łańcucha:

Wykorzystując funkcję flatMap tworzymy obiekt Obervable, który będzie emitował w momencie utworzenia subskrypcji tylko jeden element – kod odpowiedzi z serwera. Teraz pozostaje nam tylko utworzyć właściwą subskrypcję, a następnie dodać ją do obiektu, który we właściwym momencie usunie ją z pamięci. Dodajecie poniższe polecenia:

Za pomocą subskrypcji będziemy przekazywali kod odpowiedzi do funkcji showMessage(), która odpowiada za wyświetlenia odpowiedniej wiadomości na ekranie.

Możecie teraz dodać wywołanie nowo utworzonej funkcji na samym dole getCoinsFromURL():

 

 

Nasłuchiwanie zmian zachodzących w inny obiekcie

 

Jesteśmy już prawie na końcu. Pokaże Wam jeszcze jak można zapisać się do nasłuchiwana zmian zachodzących w innym obiekcie. W tym celu na ekranie CoinsDetalisVC za pomocą przycisku „Set Coin of the day” będziemy wybierać walutę, której nazwa pojawi się na samej górze ekranu MainViewVC.

Otwórzcie teraz plik CoinsDetalisVC.swift i dodajcie poniższy kod tuż pod obiektami UILabel:

Obiekt coinOfTheDay jest specjalnym rodzajem Observable, który pozwala na dodawanie nowych wartości do sekwencji. Każdy obiekt, który zapisze się do nasłuchiwania wiadomości wysyłanych przez coinOfTheDay, będzie otrzymywał tylko te, które zostały wyemitowane już po dokonaniu subskrypcji. Alternatywą dla tego rozwiązania może być BehaviorSubject lub ReplaySubject. Więcej na ich temat możecie poczytać w moim poprzednim artykule.

Wartością przekazywaną przez coinOfTheDay do MainViewController będzie Coin. Obiekt selectedCoin z kolei ma za zadnie ukrywać przed światem obiekt coinOfTheDay (który jest prywatny), tak aby przypadkiem inne obiekty nie publikowały za jego pomocą nowych event’ów. Tak jak my zrobimy to za chwilę.

To właśnie dzięki obiektowi selectedCoin, który występuje pod postacią standardowego obiektu Obervable będziemy mogli przekazać odpowiednią wartość do obiektu MainViewVC w sposób „reaktywny”.

Odszukajcie teraz w pliku CoinsDetalisVC funkcję o nazwie setCoinOfTheDay(sender:), która został już połączona z przyciskiem „Set Coin of the day”. Dodajcie do niej poniższy kod tuż nad wywołaniem funkcji showMessage(title: description):

Za pomocą powyższego kodu będziemy przekazywali nową wartość do strumienia danych emitowanych przez coinOfTheDay. Przekazywanym obiektem będzie singleCoin, który jest przypisywany do obiektu CoinsDetalisVC podczas jego inicjalizacji. Ta z kolei ma miejsce w obiekcie MainViewVC podczas wyboru odpowiedniej komórki w tabeli.

Zdaję sobie sprawę, że kod znajdujący się w funkcji setCoinOfTheDay(sender:) nie jest idealnym przykładem podejścia reaktywnego. Korzystamy z pola, które jest integralną częścią obiektu CoinsDetalisVC, a co za tym idzie „kontaktujemy się ze światem zewnętrznym”. Takich sytuacji podejście reaktywne stara się właśnie unikać. Moim zdaniem jednak nie należy się tym aż tak bardzo przejmować, bo kod idealny nie istnieje i czasami trzeba iść na kompromis. W przypadku naszej przykładowej aplikacji takie podejście w zupełności wystarczy.

Teraz pozostaje jeszcze dla porządku dodać w funkcji viewWillDisappear(animated:) poniższy kod:

Za jego pomocą wymusimy na obiekcie coinOfTheDay wyemitowanie event’u onCompleted(), który zakończy w sposób poprawny działanie Observable w chwili zamknięcia widoku CoinsDetalisVC. Ta operacja jest bardzo istotna ponieważ pomaga zapobiegać zjawisku retain cycle.

Musimy jeszcze tylko zapisać obiekt MainViewVC do nasłuchiwania zmian. Otwórzcie teraz plik MainViewVC.swift i odszukajcie na samym dole funkcję tableView(_ tableView: didSelectRowAt:). Jak zapewne wiecie funkcja ta jest wywoływana za każdym razem, gdy użytkownik wybierze daną komórkę w tabeli. Tuż nad wywołaniem funkcji navigationController?.pushViewController() dodajcie poniższy kod:

Za każdym razem, gdy użytkownik wybierze daną komórkę to zostanie utworzona nowa subskrypcja. Wartością, która będzie przekazywana w zdarzeniu on next(), będzie obiekt Coin. Będzie on reprezentował kryptowalutę, którą wybraliśmy na ekranie CoinsDetalisVC. Korzystając z obiektu Coin ustawimy wartość pola tekstowego znajdującego się na górze ekranu.

Wewnątrz bloku onNext() skorzystamy ze słabej referencji do obiektu MainViewVC, aby i tym razem uniknąć retain cycle. Pozostałe bloki zostaną wywołane w przypadku wyemitowania przez Observable określonego zdarzenia. Nie będą nam one do niczego w tej chwili potrzebne, ale zostawiłem je żebyście mogli zobaczyć jak prezentuje się komplet funkcji.

Na samym końcu musimy dodać naszą subskrypcję do obiektu DisposeBag, który usunie ją z pamięci we właściwym momencie. DisposeBag będzie dla nas automatycznie zarządzał subskrypcjami. Tuż przed usunięciem go z pamięci, każda subskrypcja z osobna zostanie anulowana poprzez wywołanie metody dispose().

Najlepszym rozwiązaniem będzie skorzystanie z obiektu umieszczonego w CoinsDetailsVC. Za każdym razem, gdy wybierzemy komórkę w tabeli zostanie w pamięci utworzona nowa subskrypcja. Dodanie jej do obiektu DisposeBag znajdującego się MainViewVC nie przyniesie oczekiwanych rezultatów, ponieważ główny kontroler będzie zwolniony z pamięci dopiero w momencie zamknięcia aplikacji. W takim przypadku DisposeBag nie będzie miał możliwości wykonania koniecznych operacji. Inaczej sprawa ma się z CoinsDetailsVC, który będzie usuwany z pamięci za każdym razem, kiedy będziemy wracali na ekran główny.

Aplikacja na tym etapie jest już gotowa. Możecie ją uruchomić i cieszyć się efektami swojej ciężkiej pracy 🤓.

 

Krótkie podsumowanie

 

Trochę w naszej aplikacji się wydarzyło. Pobraliśmy dane z serwera, przetworzyliśmy odpowiedzi, a także zmapowaliśmy dane z naszymi modelami. Z pewnością zwróciliście uwagę na fakt, że kod napisany w oparciu o paradygmat reaktywny potrafi ciągnąć się w nieskończoność i momentami być trochę mało czytelny. Niestety taki jego urok, ale ratować możecie się rozbijaniem poszczególnych fragmentów na mniejsze funkcje, tak jak w przypadku filtrowania odpowiedzi przychodzących z serwera.

Zaletą jest jednak fakt, że wszystkie operacje związane z pobieraniem danych z serwera i ich mapowaniem są wykonywane w jednym ciągu, bez konieczności korzystania z delegatów lub callback’ów, na przykład w postaci ‚escaping blocks’.

 

Słowo na drogę

 

Po zapoznaniu się z tym poradnikiem powinniście już trochę lepiej orientować się w temacie biblioteki RxSwift i jej praktycznego zastosowania. Muszę przyznać, że był to dla mnie jak do tej pory najtrudniejszy pod względem technicznym wpis. Sporo czasu zajęło mi napisanie samej aplikacji, a do tego musiałem to wszystko jakoś sensownie opisać (w sumie to dwa miesiące nad nim dłubałem 😅). Mam nadzieję jednak, że było warto i moja praca do czegoś Wam się przyda. Do następnego 🧐.

 


 

Comments

Dodaj komentarz

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