Protokoły

18 Marca 2018

desk

Dzień dobry!

Po krótkiej przerwie wracam z kolejną porcją wiedzy, bo ostatnio mniej było tutaj mojej aktywności. Ale to tylko chwilo 😁📽.

Artykuł ten zainspirowany został prośbą pewnego mało znanego programisty – blogera i to jeszcze wywodzącego się z ciemnej strony mocy (Microsoft, ueffff…. 🤮), Macieja Aniserowicza. Prośba o jakieś małe wsparcie dla niego ➡️ https://www.devstyle.pl

A tymczasem… zaczynamy. Co to są, po co są, i w ogóle co z tymi protokołami?

Protokoły jako interfejsy

Jak mawia klasyk – „Protokoły to takie interfejsy, tylko że w Swift’cie…”. Czy to prawda? Cóż, po części to oczywiście prawda. Protokoły działają bardzo podobnie jak interfejsy w popularnych językach programowania, jak Java czy C#.

Zatem, protokoły to definicja abstrakcyjnego typu posiadającego jedynie operacje, a nie dane. Zatem – tak jak w interfejsach – w protokołach będziemy implementowali metody, które mogą zostać wywołane w klasach lub strukturach, które implementują dany protokół. Proste 😁

Jak to działa w praktyce? Zobaczmy na przykładzie implementacji tabeli w UIKit.

Kiedy chcemy zaimplementować widok tabeli – UITableView – poza utworzeniem odpowiedniego obiektu klasy UITableView, musimy jeszcze dodać protokół do definicji klasy. W przypadku tabeli mamy dwa kluczowe protokołu – UITableViewDelegate oraz UITableViewDataSource. Zatem wygląda to następująco:


class MyViewController : UIViewController, UITableViewDelegate, UITableViewDataSource {
    
    var tableView: UITableView!
    
}

UITableViewDelegate i UITableViewDataSource to protokoły implementujące metody dla obsługi pewnych zdarzeń naszej tabeli. Żeby móc skorzystać z tych metody, należy przypisać naszą klasę jako Delegata dla naszej tabeli. Delegat to inaczej zmienna obiektu tableView typu – oczywiście – UITableViewDelegate (lub UITableViewDataSource), czyli naszego protokołu. Robimy to przy pomocy słowa kluczowego self:


class MyViewController : UIViewController, UITableViewDelegate,  UITableViewDataSource{
    
    var tableView: UITableView!
    
    override func loadView() {
        
        tableView.delegate = self
        tableVire.dataSource = self
    }
}

Teraz prekompilator Xcode powinien rzucić błędem. I ma racje! W tym momencie warto otworzyć oficjalną dokumentację dla UITableViewDataSource i zobaczyć, jakie metody możemy zaimplementować 🤔. Oficjalna dokumentacja dostępna jest o tutaj: https://developer.apple.com/documentation/uikit/uitableviewdatasource

I…. ?🤔

Metody obowiązkowe i opcjonalne

I okazuje się, że w Swift mamy do czyniena z czymś takim jak metody opcjonalne oraz wymagane. Oznacza to, że w każdym protokole możemy definiować takie metody, które zaimplementować musimy oraz takie, które możemy 👉 ewentualnie. Po co? Dobrze to widać właśnie na przykładzie tabelki.

Wracamy do naszego kodu z błędem prekompilatora. Mówi on o tym, że protokół zaimplementowany w naszej klasie ma metody wymagane. Są to :


func tableView(UITableView, cellForRowAt: IndexPath)
func numberOfSections(in: UITableView)
func tableView(UITableView, numberOfRowsInSection: Int)

Bez implementacji tych metod, nasza tabela nie powstanie. To znaczy powstanie, ale nie będzie w żaden sposób rozbudowywalna, ani nie pokaże żadnych naszych danych. R.I.P ☠️. Zatem te metody zaimplementować musimy, jeśli chcemy korzystać z protokołu.

Ale jeśli zagłębimy się w dokumentacje to zobaczymy szereg innych metod, które oferuje nam protokół UITableViewDataSource. Na przykład możemy obsłużyć akcję kliknięcia na dany rekord w tabelce. Zrobimy to poprzez metodę

tableView(_:didSelectRowAt:)

Jako parametr mamy dostępną tabelę, na której została wykonana akcja, oraz index (IndexPath) danej komórki, która została kliknięta. W zasadzie wszystko, czego potrzebujemy żeby obsłużyć kliknięcia.

Dzięki temu, że niektóre metody są opcjonalne i nie musimy ich implementować, nasza klasa posiada tylko niezbędne metody i mamy dużo mniej zbędnego kodu w pliku z klasą 📚 – porządek w kodzie przede wszystkim. Dla mnie bomba 😁.

Własny protokół

No dobra, tabela ma protokoły. Kolekcje mają swoje protokoły. A jak w takim razie napisać swój protokół? Zobaczmy to na przykładzie – powiedzmy że chcemy otworzyć nowe okno z polem tekstowym, a po wpisaniu słowa w te pole ➡️ wyświetlić wpisany tekst na poprzednim ekranie. To jest dobre miejsce na protokół, który dostarczy nam daną z widoku z góry stosu na widok niżej.

Let’s get this work 🛠.

Na początek stwórzmy projekt aplikacji iOS w Xcode. Następnie dodajemy kolejny widok w Storyboard:

storyboard

Na pierwszym ekranie umieszczamy UILabel oraz przycisk do przejścia do kolejnego ekranu. Na drugim widoku umieszczamy UITextField oraz przycisk do zatwierdzenia zdarzenia. Ok 😁 UI gotowy, przejdźmy do kodu.

Najpierw stwórz klasę UIViewController do obsługi drugiego ekranu. U mnie to będzie SecondViewController. Dodajemy outlet UITextField oraz IBAction dla przycisku Done. Kiedy mamy gotowe wszystko, to możemy zainicjować nasz protokół. Ogólna zasada jest bardzo prosta ➡️ protokół inicjujemy za pomocą słowa kluczowego protocol oraz nazwy protokołu:

protocol SecondViewControllerDelegate {
}

BEST PRACTICE: Są dwie dobrego praktyki implementacji protokołów. Jeżeli protokół odnosi się konkretnie do jednej klasy (tzn. jest z nią bezpośrednio związany), to powinniśmy umieszczać go na samej górze, nad deklaracją klasy. Jeżeli zaś protokół może być używany w różnych częściach programu, to możemy zadeklarować go w oddzielnym pliku z kodem. Ja robię to na górze mojej klasy, bo metoda będzie specyficzna dla tego widoku.

Nasz protokół nie ma jeszcze żadnej metody. Musimy zatem zadeklarować metodę, które będzie mogła być nadpisana w klasie obsługującej pierwszy widok. Chcemy, żeby po kliknięciu na przycisk DONE wyświetlić wpisany w pole tekst w pierwszym widoku. Oraz – rzecz jasna – zamknąć widok z polem.

Niech zatem metoda nazywa się textSubmitted i przekazuje parametr text:


protocol SecondViewControllerDelegate {
    func textSubmitted(text: String)
}

Git. Teraz musimy wiedzieć, kiedy ta metoda ma być wywołana. Po kliknięciu w przycisk DONE. Ok, najpierw potrzebujemy instancji naszego protokołu. Zwyczajowo przyjęło się, że protokoły danych klas to delegaci (delegate). Oczywiście jest to tylko best practice, a nazwa zmiennej może być dowolna. Zatem inicjujemy zmienną opcjonalną w klasie SecondViewController i deklarujemy ją słowem var (będziemy ją modyfikować z poziomu pierwszego widoku. ).


class SecondViewController: UIViewController {

    var delegate: SecondViewControllerDelegate?
    
    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
    }

No i super, teraz zostaje nam ją wywołać. Zrobimy to w IBAction podpiętym do przycisku DONE:


@IBAction func submit(_ sender: Any) {
      delegate?.textSubmitted(text: textField.text)
}

NOTE:Przykład dotyczy modelu delegatów w Swift. Taki zapis, jaki popełniliśmy nie jest do końca prawidłowy. Dlaczego? Ponieważ referencja do delegata jest w tej chwili string. To znaczy, że jeżeli raz przypiszemy zmiennej jakąś wartość, to będzie ona trzymana w pamięci aż do “ubicia” aplikacji. Dlatego dobrą praktyką w przypadku delegatów jest deklarowanie ich jako weak, żeby zapobiegać tzw. retain cycle.

Żeby móc zadeklarować delegata jako weak, musimy najpierw powiedzieć protokołowi, że może on być używany wyłącznie przez klasy:


protocol SecondViewControllerDelegate: class {
    func textSubmitted(text: String)
}

a w ViewController:


class SecondViewController: UIViewController {

    weak var delegate: SecondViewControllerDelegate?
    
    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
    }

Ok, teraz wracamy do pierwszego widoku, czyli ViewController. Ponieważ to tutaj ma zostać wykonana akcja prezentacji naszego tekstu, tutaj też musimy nadpisać metodę z naszego protokołu. Zaczynamy od dodania go w miejscu deklaracji naszej klasy ViewController:


class ViewController: UIViewController, SecondViewControllerDelegate {

Od razu prekompilator – podobnie jak było w przypadku UITableView – krzyknie na nas, żeby doimplementować wymagane metody. W naszym przypadku trzeba nadpisać metodę textSubmitted:


func textSubmitted(text: String?) {
        // TODO
 }

Ok. Nasza metoda dostarcza nam text wpisany w drugim widoku. Text jest zmienna typu String optional, co oznacza, że może być nil. Rozpakowujemy ją zwyczajowo strukturą if let, a następnie wyświetlamy w labelce:


class ViewController: UIViewController, SecondViewControllerDelegate {
    
    @IBOutlet weak var label: UILabel!
    
    func textSubmitted(text: String?) {
        guard let text = text else { return }
        self.label.text = text
    }
}

NOTE: Mimo że parametr text obiektu label również jest optional (może przyjąć nil), to dobrą praktyką jest rozpakowywanie (unwrap) zmiennych opcjonalnych.

Ok, chyba wszystko gotowe. Ready? …. Nie. 😁

Nie zrobiliśmy dwóch rzeczy. Po pierwsze – musimy jeszcze obsłużyć cofnięcie ekranu drugiego, po kliknięciu przycisku DONE. To akurat prosta sprawa.


@IBAction func submit(_ sender: Any) {
        delegate?.textSubmitted(text: textField.text)
        self.dismiss(animated: true, completion: nil)
}

I teraz rzecz najważniejsza – musimy podać delegata dla widoku, który nadpisuje naszą metodę z protokołu. Zrobimy to w momencie otwierania SecondViewController:


override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    guard let destination = segue.destination as? SecondViewController else { return }
    destination.delegate = self
}

I teraz wszystko mega działa 😎!

Metody opcjonalne

Jak wspominałem wcześniej, w protokołach możemy implementować metody, które są opcjonalne (nie muszą zostać nadpisane w nadklasie).

🤔DLACZEGO? PO CO?

1️⃣ – bo nie wszystkie metody są nam potrzebne. Kiedy zobaczymy na protokół UITableViewDataSource to widzimy, że nie wszystkie metody są obowiązkowe. Niektóre dotyczą akcji związanych z różnymi działaniami – możemy np. powiedzieć co ma się stać po kliknięciu rekordu, jak ma wyglądać nagłówek lub stopka dla każdej sekcji itp.

2️⃣ – ponieważ metody te są opcjonalne, nie musze być nadpisane. W naszym pliku z klasą panuje większy porządek, bo nie ma tam zbędnych, pustych metod. Wszystko jest bardziej przejrzyste.

Implementacja metod opcjonalnych jest jednak nieco… dziwna i można to zrobić na dwa sposoby. Ale od początku – deklaruję w moim protokole opcjonalną metodę sayHello():

optional func sayHello()

Prekompilator rzuca mi błąd ➡️ 'optional' can only be applied to members of an @objc protocol

Swift 4 wprowadził możliwość obsługi właściwości języka Objective-C oraz adnotację @objc którą deklaruje się właśnie chęć użycia takiej właściwości. Możemy zatem zadeklarować nasz protokół (i wszystkie jego metody) właśnie tak:


@objc protocol SecondViewControllerDelegate {
    @objc func textSubmitted(text: String?)
    @objc optional func sayHello()
}

Drugim sposobem jest stworzenie rozszerzenia do naszego protokołu (więcej o rozszerzeniach przeczytasz tutaj ➡️ https://swiftly.pl/rozszerzenia-typow-extensions/). W tym przypadku nie musimy deklarować metody ze słowem kluczowym optional, a jedynie nadpisać ją w rozszerzeniu, o tak:


extension SecondViewControllerDelegate{
    func sayHello(){
        //
    }
}

Ogólnie nie jestem zwolennikiem tego rozwiązania, tym bardziej że Swift 4 przyszedł do nas właśnie z natywną obsługą właściwości języka Objective-C. Niemniej nadal sporo developerów tworzy metody opcjonalne w ten sposób. Często zdarza się, że programiści implementują w ten sposób default’owe funkcje. Wtedy takie zastosowanie ma sens, bo zapobiega redundancji naszego kodu.

Inne właściwości protokołów

Protokoły, podobnie jak typy, klasy czy struktury w Swift mogą mieć swoje rozszerzenia. Pokazałem to na przykładzie naszej aplikacji z metodą sayHello(), która może zostać nadpisana właśnie w rozszerzeniu, przez co nie trzeba jej implementować w nadklasie.

Protokoły, w odróżnieniu od interfejsów, mogą posiadać własności (properties).

Protokoły mogą modyfikować instancje, w których zostają zaimplementowane poprzez metody zadeklarowane jako mutating.

Oczywiście, jak zawsze po bardziej zaawansowane przykłady i więcej wiedzy z zakresu protokołów – zachęcam do zgłębiania oficjalnej dokumentacji pod adresem: https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Protocols.html

Podsumowanie

Protokoły to naprawdę bardzo, ale to bardzo użyteczna rzecz, z którą przychodzi język Swift. Moim zdaniem są dużo bardziej funkcjonalne niż interfejsy np. W Javie. Co więcej protokoły są sercem każdej dużej aplikacji iOS zawarte w standardzie Protocol Oriented Programming (o którym wkrótce również na naszym blogu 💪). Warto zatem już na wstępie nauki języka dobrze przyswoić sobie ich sposób działania i egzystowania w ekosystemie.

Sam schemat tworzenia i zarządzania delegatami, które tutaj użyte zostały tylko dla przykładu, to temat na odrębny artykuł, który w niedługim czasie poczynię 😁.

Uff… dotrwałeś do końca! Jeśli ten artykuł był dla Ciebie pomocny to daj proszę znać w komentarzu lub prywatnie na social media 😅! Będę bardzo wdzięczny za każdą przychylną i mniej przychylną opinię, żeby tworzyć jeszcze lepszy kontent dla Ciebie.

Tymczasem dzięki za przeczytania i zapraszam do pogadania:

Twitter

Instagram

Snapchat

Lub wpadnij na fanpage na Facebook’u!

Dzięki i do następnego wpisu!

@jkornat

Protokoły

Dodaj komentarz

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