30.08.2019

Serdecznie witam Cię... ponownie ☺️. Trochę czasu minęło nie tylko od ostatniego wpisu na tym blogu, ale też o samej konferencji WWDC. Jak pewnie się domyślacie (albo i nie 😅), pochłonęło mnie na nowo kształcenie z natywnego SDK, zwanego SwiftUI. To dlatego nie było tutaj wpisów od jakiegoś czasu, ale to nic, wracamy z nową porcją wiedzy. A nie da się ukryć - jest było i nadal jest co zgłębiać.

“SwiftUI, czyli wszystko od początku”

Przed wprowadzeniem w temat mała zapowiedź 😁 🎥:
5 Listopada A.D. 2019 wystąpię na lokalnej edycji konferencji 4Developers w ramach prowadzonej tam ścieżki Mobile. Będę tam produkował się na temat tego, co dalej po tegorocznym WWDC. Tematem prelekcji będzie: “SwiftUI – czyli znowu wszystko od początku”. Będzie sentymentalnie, technicznie i merytorycznie.

Combine

No dobrze, czym zatem jest Combine framework? Combine to nowy framework zaprezentowany przez Apple na tegorocznym WWDC 2019. Dostarcza on deklaratywne API do “procesowania” danych w czasie. Apple opisuje to jako: Customize handling of asynchronous events by combining event-processing operators. Na pierwszy rzut oka to może okazać się… lekko niezrozumiałe 🤔, ale po przeczytaniu tego postu do końca mam nadzieję że lepiej zrozumiesz, do czego ten “kombajn 🚜” służy.

Combine vs Rx

Tak naprawdę Combine może (i powinien) być wprost porównywalny do takich bibliotek jak RxSwift czy ReactiveSwift (dawniej ReactiveCocoa). Pozwala on na pisanie kodu reaktywnego za pomocą deklaratywnego, natywnego (w końcu 😀) API. Takie podejśćie deklaratywne – w przeciwieństwie do imperatywnego, oznacza opisanie warunków, jakie dany fragment programu ma spełniać na końcu zamiast pisać sekwecyjnie – co ma się po czym wykonać, aby ten efekt uzyskać. Natomiast języki FRP (Functional Reactive Programming) pozwalają na procesowanie danych w czasie, czy na pracę z danymi w tzw. strumieniach. Przykładem takich danych są praktycznie wszystkie operacje asynchroniczne, np. zapytania sieciowe czy zdarzenia ekranowe z poziomu interfejsu użytkownika. Czy zatem Combine jest bezpośrednim, natywnym rywalem zewnętrznych bibliotek pokroju RxSwift? Tak. Jeśli decydujemy się na użycie Combine, to 3rd party wydają się zbędne, gdyż Combine pokrywa praktycznie wszystkie funkcjonalności tych bibliotek.

Podstawowe elementy framework’a

Żeby lepiej zrozumieć działanie omawianego framework’a, najpierw trzeba zrozumieć jest podstawowe elementy. Jak możesz się domyślić – będą one odpowiadały bezpośrednio elementom innych popularnych bibliotek reaktywnych. Ale first things first.
Publishers & Subscriers
API Combine dostarcza nam dwa podstawowe elementy, zwane Publisher’ami i Subscriber’ami (uwielbiam spolszczać to wszystko na potrzeby tego bloga 🤗🇵🇱💪). Jeśli korzystałeś z RxSwifta, to najłatwiej jest zrozumieć to tak:
  • Publisher = Observable
  • Subscriber = Observer
Różne nazewnictwo, ale działanie i przeznaczenie dokładnie takie samo: Publisher “eksponuje” zmienne, które mają być procesowane, a Subscriber zapisuje się (subscribe) na te zdarzenia emitowane przez Publisher’a, aby otrzymywać informacje o zmianach. Wydaje się to dość logiczne, choć dla osób niezaznajomionych z podstawowym paradygmatem programowania reaktywnego może być jeszcze lekko pogmatwane 🤪. Postaram się to wykładać w najbliższym czasie.

Praktyczne metody wykorzystania

Najlepszym przykładem obrazującym cały potencjał działania Comibne w opraciu o Foundation są Notyfikacje KVO. Foundation posiada już odpowiednie rozszerzenia do niektórych klas, aby były one gotowe out-of-the-box do wykorzystania razem z Combine. Tak np. jest z klasą Notification. W zastosowanym przykładzie będziemy chcieli zapisać się na daną tekstową, przychodzącą z notyifkacji:
import Combine
        
        extension Notification.Name {
            static let textData = Notification.Name("text_data")
        }
                        
        let textDataPublisher = NotificationCenter.Publisher(center: .default, name: .textData, object: nil)
        
Publisher nasłuchuje na przychodzące notyfikacje dla textDataPublisher. No dobrze, ale przecież teraz nic się nie wydarzy nawet jeśli notyfikacja przyjdzie, bo przecież nikt nie zapisał się na te aktualizacje. Dodajemy zatem naszego Subscriber’a:
let textLabel = UILabel()
        let lastTextDataSubscriber = Subscribers.Assign(object: textLabel, keyPath: \.text)
        textDataPublisher.subscribe(lastTextDataSubscriber)
        
Dostaniemy błąd mówiący o tym, że nasz publisher nie jest typu String, więc żeby to zadziałało to musimy jeszcze emitowaną wartość zwrócić jako String. A pomoże nam w tym operator map:
let textDataPublisher = NotificationCenter.Publisher(center: .default, name: .textData, object: nil).map { (notification) -> String? in
            guard let response = notification.object as? String else { return "" }
            return response
        }
        
I teraz testujemy:
let exampleText = "Combine is awsome!"
            
        NotificationCenter.default.post(name: .textData, object: exampleText)
            
        print("Last post is: \(textDataPublisher.text!)") // Combine is awsome!
        
Kiedy jest przysłana jakaś wiadomość, kontrolka (Label) jest aktualizowana o nową daną. O to nam właśnie chodziło w tym ćwiczeniu 😁.
Zasady zapisywania się na zdarzenia
Zasady zapisywania się na zdarzenia są również zbieżne z zasadami obowiązującymi w programowaniu reaktywnym, a zatem postępujemy według nich również w przypadku korzystania z Combine:
  • Subscriber może zapisać się tylko na jedno zdarzenie (może zapisać się tylko raz na coś).
  • Zero lub więcej wartości może być publikowanych.
  • Maksymalnie jedna funkcja dopełniająca (completion) będzie wywołane.

Combine a zarządzanie pamiecią

Temat zarządzania pamięcią w Combine zostanie jeszcze poruszony w najbliższych wpisach rozszerzających ten temat, jednak na potrzeby pierwszej implementacji zaznaczę jedną, acz bardzo ważną rzecz ⚠️. RxSwift dostarczał nam DisposeBag, czyli kontener zawierający nasze subskrybcje, który był czyszczony (ręcznie) przy każdym zwalnianiu pamięci. I znowu – Combine przychodzi z podobnym mechanizmem, zamkniętym w klasie AnyCancellable. Klasa ta woła metodą cancel() przy każdej deinicjalizacji instancji oraz upewnia się, że wszelkie subskrybcje zostały zakończone. Bez tej implementacji możemy bardzo łatwo skończyć z tzw. retain cycle, czyli rekurencyjnym zapętleniem w pamięci. Dobrym przykładem może być formularz kończący się akceptacją polityki prywatności:
class FormViewController: UIViewController {
        
            @Published var isSubmitAllowed: Bool = false
            private var switchSubscriber: AnyCancellable?
                
            @IBOutlet private weak var acceptSwitch: UISwitch!
            @IBOutlet private weak var submitButton: UIButton!
                
            override func viewDidLoad() {
                super.viewDidLoad()
                switchSubscriber = $isSubmitAllowed.receive(on: DispatchQueue.main).assign(to: \.isEnabled, on: submitButton)
            }
                
            @IBAction func didSwitch(_ sender: UISwitch) {
                isSubmitAllowed = sender.isOn
            }
        }
        

Konkluzje

Podsumowując, Combine to deklaratywne API służące wprowadzeniu reaktywnego paradygmatu programowania do natywnego świata platform Apple. Ten artykuł to jedynie lakoniczny wstęp 😅 do całego zagadnienia. W najbliższych tygodniach będę będą pojawiały się kolejne artykuły dotyczące Combine. Postaram się również znaleźć czas na opis innych zmian, jakie wprowadzi oficjalnie Apple już w niedalekiej przyszłości, a które to są pokłosiem ostatniej konferencji WWDC. W kolejnych częściach rozszerzę temat Combine framework m.in. o:
  • Błędy i strumienie
  • Publishers & Subscribers – rozszerzenie
  • Debbugowanie strumieni
  • Operatory dla publisherów
  • Zarządzanie pamięcią
Tymczasem dziękuję Ci za poświęcony czas i do następnego 🚀!
Czym jest Combine framework?

Dodaj komentarz

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