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

Komentarze 4 thoughts on “Protokoły

  • Avatar
    13 października, 2019 z 06:25
    Permalink

    Dzięki. Wreszcie to zrozumiałem, prosta sprawa w sumie

    Powtórz
    • Jakub Kornatowski
      13 października, 2019 z 06:40
      Permalink

      Proszę bardzo 🙂

      Powtórz
  • Avatar
    26 listopada, 2019 z 12:38
    Permalink

    Dzięki za ten artykuł o protokołach. I dziękuję w ogóle za blog o Swifcie. W końcu mogłem poczytać o temacie trochę po polsku. 🙂 Mamy bardzo dużo wartościowych materiałów o Swift w necie po angielsku, a jeśli chodzi o polskie materiały to jest słabo. Rozumiem, że oficjalnym językiem każdego programisty jest angielski i nie mam z tym kłopotu, ale jest to naprawdę miła odmiana, szczególnie przy pewnych złożonych problemach, gdzie przydaje się zmiana perspektywy.
    Czy artykuł o POP się kiedyś pojawi? 🙂

    Powtórz
    • Jakub Kornatowski
      26 listopada, 2019 z 13:28
      Permalink

      Cześć Marek,

      Dzięki za twój komentarz :). Tak, jest już nawet draft tego wpisu ale ponieważ dużo rzeczy jest skolejkowanych to ciągle schodzi to na dalszy plan. Ale skoro jest zainteresowanie to postaram się niedługo do opublikować!

      All best!

      Powtórz

Dodaj komentarz

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