Über unsMediaKontaktImpressum
Thomas Sillmann 08. März 2016

Das Key-Value Observing: iOS-Entwicklung mit MVC (Model-View-Controller)

Im ersten Teil dieses Beitrags haben wir die Funktionsweise von MVC sowie das Notifications-System von iOS kennengelernt. In diesem zweiten Teil knüpfe ich direkt daran an und stelle Ihnen zu Beginn die zweite Technik zur Kommunikation zwischen Model und Controller neben den Notifications vor: Das Key-Value Observing.

Key-Value Observing

Notifications sind die eine Möglichkeit, eine Kommunikation zwischen Model und Controller herzustellen. Die andere dazugehörige Technik in der iOS-Entwicklung nennt sich Key-Value Observing (kurz KVO).

Während bei Notifications zwei Stellen aktiv werden (die eine sendet eine Nachricht, die andere lauscht darauf), ist das beim Key-Value Observing anders. Hier registriert sich ein Objekt auf eine bestimmte Property eines anderen Objekts und wird informiert, sobald sich eben jene Property ändert. Im Vergleich zu den Notifications fällt somit die explizite Sendestelle weg.

Damit Key-Value Observing funktioniert, ist es wichtig, das alle zugehörigen Klassen von NSObject erben, denn diese Klasse aus dem Foundation-Framework bringt die grundlegende Funktionalität für KVO mit; ohne NSObject kann KVO nicht verwendet werden.

Ähnlich wie bei Notifications registriert sich ein Objekt bei KVO, um über Änderungen informiert zu werden und darüber eigene Aktionen durchzuführen. Dabei definiert nicht NSObject selbst die für KVO benötigten Methoden. Stattdessen sind diese im Protokoll NSKeyValueObserving definiert, zu dem NSObject konform ist. Möchte nun ein Objekt einer Klasse über die Änderungen einer Property eines anderen Objekts informiert werden, so muss sich dieses als Observer registrieren. Dazu dient die folgende Methode des NSKeyValueObserving-Protokolls:

func addObserver(_ observer: NSObject, forKeyPath keyPath: String, options options: NSKeyValueObservingOptions, context context: UnsafeMutablePointer<Void>)

Diese Methode wird auf dem Objekt aufgerufen, von dem eine Property überwacht werden soll. Damit diese Methode auf eben jenem Objekt aufgerufen werden kann, ist es – wie bereits angemerkt – wichtig, das jenes Objekt in jedem Fall von NSObject erbt; nur so steht diese Methode zur Verfügung.

Betrachten wir dabei die einzelnen Parameter einmal im Detail:

  • observer: Beim observer handelt es sich um das Objekt, das über Änderungen des Objekts, über das die Methode aufgerufen wird, informiert werden möchte.
  • keyPath: Der keyPath entspricht dem Namen der Property, die auf dem Objekt, über das die Methode aufgerufen wird, überwacht werden soll. Diese Property wird als einfacher String übergeben. Dabei können auch mehrere Properties über den keyPath miteinander durch Trennung per . verkettet werden; dazu später mehr.
  • options: Die Optionen definieren, welche Informationen bei einer Änderung der zu überwachenden Property an den observer übergeben werden. Zwei typische Optionen sind .New und .Old, welche festlegen, dass bei einer Änderung der neue Wert (.New) beziehungsweise der vorherige Wert (.Old) mit übergeben werden.
  • context: Der Context dient zur Identifizierung einer spezifischen Property-Überwachung. Das liegt daran, das ein Objekt ja mehrere verschiedene andere Objekte und Properties überwachen könnte. Wenn es nun zu einer Änderung kommt, müssen Sie irgendwie feststellen, welche der überwachten Objekte und Properties sich nun geändert hat, und dazu dient der context.

Äquivalent zu der Methode addObserver:forKeyPath:options:context: verfügt das NSKeyValueObserving-Protokoll über eine weitere Methode, um gesetzte Observer wieder zu entfernen. Das Prinzip ist dabei dasselbe wie bei den Notifications: Gesetzte Observer müssen an geeigneter Stelle auch wieder entfernt werden, damit nicht nach Freigabe der entsprechenden Objekte ein möglicher Aufruf erfolgt, der ins Leere und somit zu einem Absturz führt. Die Methode removeObserver:forKeyPath:context: ist wie folgt im Protokoll definiert:

func removeObserver(_ observer: NSObject, forKeyPath keyPath: String, context context: UnsafeMutablePointer<Void>)

Die Parameter observer, keyPath und context sind dabei äquivalent zu den gleichnamigen Parametern aus der Methode addObserver:forKeyPath:options:context:.

Der context-Parameter

Wer sich die Deklaration des context-Parameters in den Methoden addObserver:forKeyPath:options:context: und removeObserver:forKeyPath:context: etwas genauer angesehen hat, war womöglich vom Typ dieses Parameters etwas irritiert. Dabei handelt es sich nämlich um ein Objekt vom Typ UnsafeMutablePointer<Void>. Doch was ist das eigentlich für ein Typ?

Einfach gesagt, handelt es sich dabei um einen Verweis auf eine Speicherstelle, die zur eindeutigen Identifizierung des Observers dient. Laut Swift-Dokumentation lässt sich ein solches context-Objekt aber sehr einfach erstellen. Es reicht dabei, eine einfache Variable zu erzeugen und dieser einen Integer zuzuweisen. Damit wird eine Speicherstelle für diese Variable reserviert, und genau diese wird dann als Parameter für context verwendet. Dabei ist zu beachten, für context die Speicheradresse und nicht das Objekt selbst zu übergeben. Das wird mithilfe des &-Operators bewerkstelligt, der dem übergebenen Parameter vorangestellt wird. Wie das konkret aussieht, sehen wir gleich in einem vollständigen Beispiel zu KVO.

Kennzeichnung von zu überwachenden Properties

Bevor wir nun den Einsatz der Methoden des NSKeyValueObserving-Protokolls in der Praxis betrachten, sei zuvor noch ein weiteres wichtiges Detail vorangestellt: Properties, die Sie mittels KVO überwachen möchten, müssen zwingend mit dem Schlüsselwort dynamic gekennzeichnet werden. Nur dann ist eine Überwachung der entsprechenden Property möglich. Das folgende Beispiel zeigt eine derartige Deklaration:

dynamic var propertyToObserve = "Property to observe"

Ebenso wichtig ist – wie bereits beschrieben –, dass die Klasse, die über mindestens eine zu überwachende Property verfügt, auch in jedem Fall von der Klasse NSObject erbt, damit KVO verwendet werden kann.

Abfangen von Änderungen mittels KVO

Zum Abschluss der KVO-Funktionsweise bleibt nun noch eine Frage zu klären: Wie wird man über Änderungen der zu überwachenden Properties informiert? Schließlich geben wir im Gegensatz zu Notifications keinerlei Methoden beim Setzen der Observer an, die festlegen, welche Aktionen bei Änderung der zu überwachenden Properties ausgeführt werden.
Hier kommt eine weitere Methode des NSKeyValueObserving-Protokolls ins Spiel, die im Folgenden deklariert ist:

func observeValueForKeyPath(_ keyPath: String?, ofObject object: AnyObject?, change change: [String : AnyObject]?, context context: UnsafeMutablePointer<Void>)

Diese Methode muss in den Klassen überschrieben werden, die Properties überwachen und über entsprechende Änderungen informiert werden möchten. Diese Methode ist die zentrale Anlaufstelle, die dann bei Änderung eben jener überwachten Properties automatisch vom System aufgerufen wird. Und darin gilt es, die gewünschten Aktionen durchzuführen, äquivalent zu den definierten Selectors bei Notifications.

Bei Überschreiben dieser Methode ist eine sehr wichtige Sache zu beachten: Diese Methode wird auch von Systemkomponenten von Apple verwendet. Daher gilt es bei Aufruf dieser Methode zu prüfen, ob diese tatsächlich wegen einer von uns überwachten Property aufgerufen wurde oder nicht. In letzterem Fall liegt es dann an uns, dieselbe Methode auf der Superklasse mittels super aufzurufen. Tun wir das nicht, kann das zu Systeminstabilitäten bis hin zu Abstürzen der eigenen App führen. Dazu nutzen wir das übergebene context-Objekt der Methode observeValueForKeyPath:ofObject:change:context:, die wir mit dem context-Objekt abgleichen, welches wir bei der Registrierung der Observer verwendet haben.

Das change-Dictionary enthält darüber hinaus Informationen zur Änderung der überwachten Property. Welche Informationen das Dictionary enthält, ist davon abhängig, welche Optionen beim Observer für den Parameter options gesetzt wurden.

KVO in der Praxis

Manches mag bisher noch recht kryptisch klingen, daher betrachten wir einmal ein konkretes Beispiel zu KVO, das alle bisher genannten Aspekte des Key-Value Observing im Detail beleuchtet.

Listing 1: Vollständiges KVO-Beispiel

class Person: NSObject {
    let firstName: String
    let lastName: String
    init(firstName: String, lastName: String) {
        self.firstName = firstName
        self.lastName = lastName
    }
}

class Company: NSObject {
    dynamic var employees = [Person]()
    func addEmployee(employee: Person) {
        employees.append(employee)
    }
}

class EmployeeViewController: NSObject {
    let company = Company()
    private var companyContext = 0
    override init() {
        super.init()
        company.addObserver(self, forKeyPath: "employees", options: .New, context: &companyContext)
    }
    override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
        if context == &companyContext {
            // Reload view.
        } else {
            super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
        }
    }
    deinit {
        company.removeObserver(self, forKeyPath: "employees", context: &companyContext)
    }
}

Betrachten wir einmal einige Details dieser Implementierung. Die Klasse EmployeeViewController definiert nun eine eigene Property company vom Typ Company und ein Context-Objekt namens companyContext; dieses wird für die eindeutige Identifizierung des Observers benötigt. Ziel ist es – wie im vorigen Notifications-Beispiel –, über Änderungen des employees-Array informiert zu werden. Zu diesem Zweck registriert sich die Klasse EmployeeViewController in ihrem Initializer als Observer für die eigene company-Property. Da es sich bei der zu überwachenden Property des Company-Typs um employees handelt, wird diese als keyPath-Parameter in Form eines Strings übergeben. Als Context wird die Property companyContext verwendet, deren Adresse mithilfe des &-Operators übergeben wird. Innerhalb des Deinitializers wird dann noch die passende Methode zum Entfernen des gesetzten Observers aufgerufen.

Um nun mögliche Änderungen der Property employees des company-Objekts abzufangen, muss innerhalb der Klasse EmployeeViewController, die als Observer fungiert, die Methode observeValueForKeyPath:ofObject:change:context: überschrieben werden. Deren Implementierung prüft das übergebene context-Objekt gegen die Speicheradresse der companyContext-Property. Ist diese Prüfung erfolgreich, wissen wir, dass sich die Property employees geändert hat und wir können entsprechend darauf reagieren (beispielsweise durch Aktualisierung der Ansicht, um alle aktuellen Mitarbeiter anzuzeigen). Sollte die Prüfung nicht erfolgreich sein, wissen wir ebenso, dass diese Methode aufgrund einer anderen Änderung, die vom System ausging, aufgerufen wurde. In diesem Fall müssen wir sicherstellen, dann die entsprechende Methode der Superklasse mittels super aufzurufen.

Ebenfalls beachtet werden muss die Deklaration der zu überwachenden Property employees in der Klasse Company als dynamic, da nur dann eine Überwachung möglich ist.

Key-Path

Zum Abschluss der Vorstellung von KVO möchte ich noch einmal auf den Key-Path eingehen, der den "Pfad" zur überwachenden Property beschreibt. Im Beispiel aus Listing 1 war dieser Key-Path sehr einfach, da er sich direkt auf die Property employees der zu überwachenden Klasse Company bezog. Doch mithilfe des Key-Path können auch ganze Pfade von Properties "aneinandergekettet" werden. Listing 2 zeigt dazu ein abgewandeltes Beispiel.

Listing 2: Beispiel-Klassen zur Nutzung des Key-Path

class Person: NSObject {
    let firstName: String
    let lastName: String
    dynamic var tasks: [String] = [String]()
    init(firstName: String, lastName: String) {
        self.firstName = firstName
        self.lastName = lastName
    }
}

class Company: NSObject {
    let name: String
    var ceo: Person
    init(name: String, ceo: Person) {
        self.name = name
        self.ceo = ceo
    }
}

class CompanyViewController: NSObject {
    var company = Company(name: "MyCompany", ceo: Person(firstName: "Thomas", lastName: "Sillmann"))
    private var companyContext = 0
    override init() {
        super.init()
        company.addObserver(self, forKeyPath: "ceo.tasks", options: .New, context: &companyContext)
    }
    override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
        if context == &companyContext {
            // Reload view.
        } else {
            super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
        }
    }
    deinit {
        company.removeObserver(self, forKeyPath: "ceo.tasks", context: &companyContext)
    }
}

Die Klasse Company verfügt nun noch über eine weitere Property person, die wiederum die neue Property tasks besitzt. Die Klasse CompanyViewController überwacht nun die Property tasks und greift dazu über den Key-Path zu, indem es sich von der Property ceo zu tasks "durchhangelt".

In solch einem Fall ist es auch ausreichend, wenn die "finale" zu überwachende Property (also in diesem Beispiel tasks) als dynamic deklariert wird; die vorigen Properties des Key-Path müssen nicht zwingend als dynamic deklariert werden.

Vor- und Nachteile von Notifications und KVO

Sie haben nun die beiden typischen Techniken zur Kommunikation zwischen Model und Controller in der iOS-Entwicklung kennengelernt. Bleibt die Frage: Wann sollten Sie welche der beiden Techniken am besten einsetzen?

Wann immer Sie sich innerhalb einer Klasse für die Werte von Properties anderer Objekte interessieren, die sie innerhalb dieser Klasse selbst referenzieren, ist KVO sicherlich zunächst die beste Lösung. Dann nämlich stehen Ihnen alle Informationen und Objekte zur Verfügung, die Sie benötigen, um einen passenden Observer zu registrieren und Änderungen an den gewünschten Properties abzufangen.

Notifications hingegen streuen weiter. Selbst wenn Sie keine Referenz auf ein Objekt besitzen, das eine für Sie interessante Notification sendet, können Sie diese dennoch abfangen und entsprechende Aktionen durchführen. Generell ist das Verwenden und Senden von Notifications immer dann sinnvoll, wenn eine Aktion ausgeführt wird, die weitreichenden Einfluss in einem Projekt besitzt. Auch wenn es viele Stellen in einem Projekt gibt, die sich für eine bestimmte Aktion interessieren, so ist es einfacher, eine Notification zu senden und an den gewünschten Stellen abzufangen, als an jeder benötigten Stelle mittels KVO die zugehörigen Eigenschaften abzufragen.

Zu guter Letzt erlauben es Notifications, weitere Informationen wie ein Objekt und ein User-Info-Dictionary zu senden. Sollte das auch in Ihrem Fall nötig sein, ist sehr wahrscheinlich die Nutzung von Notifications der von KVO vorzuziehen. Ebenfalls können Notifications an jeder beliebigen Stelle gesendet werden. KVO hingegen erlaubt "nur" die Überwachung von Properties.

Kommunikation zwischen View und Controller

Nachdem wir nun wissen, wie Model und Controller in einer iOS-App miteinander kommunizieren können, bleibt noch die Frage, welche Möglichkeiten der Kommunikation es typischerweise zwischen View und Controller gibt. Auch hier stehen zwei verschiedene Techniken zur Verfügung: Target-Action und Delegation. Beide Verfahren sind sehr unterschiedlich und haben auch verschiedene Einsatzgebiete.

Betrachten wir dabei zunächst einmal Target-Action. Wie der Name bereits andeutet, gibt es bei dieser Technik zwei essenzielle Bestandteile. Das Target verweist auf ein Objekt, welches eine View zu einem bestimmten Zeitpunkt aufruft um eine Aktion in Form einer Methode auszuführen; die sogenannte Action. Nehmen wir als Beispiel einen Button, ein typisches View-Element. Wenn ein Button betätigt wird, soll eine bestimmte Aktion ausgeführt werden. Welche Aktion das ist, hängt davon ab, wo der Button definiert ist und was er tun soll; schließlich hat so ziemlich jeder Button eine andere Aktion auszuführen. Damit ein spezifischer Button nun weiß, was er bei einer Betätigung tun soll, werden ihm zwei Informationen übergeben: Einmal erhält der Button eine Referenz auf ein Objekt (typischerweise ein Controller), der den Code enthält, der bei Betätigung ausgeführt werden soll. Diesen Code, der typischerweise in einer eigenen Methode implementiert ist, erhält der Button ebenfalls in Form eines Selectors. Wird der Button nun betätigt, ruft er schlicht und einfach aus seinem Target (dem zugewiesenen Controller) die Action (die Methode mit der gewünschten auszuführenden Implementierung) auf. Damit ändert ein Button ganz schnell sein Verhalten, abhängig davon, welches Target und welche Action ihm zugewiesen werden.

Target-Action in der Praxis

Um die praktische Verwendung von Target-Action zu verstehen, sehen wir uns einmal ein konkretes Beispiel dazu näher an. Zu diesem Zweck soll eine eigene vereinfachte Button-Klasse erstellt werden, die in ihrem Initializer ein passendes Target mitsamt passender Action erhält und diese Informationen in Properties speichert. Wird dann eine bestimmte Methode der Button-Klasse aufgerufen, greift die Klasse auf ihr Target zu und ruft darauf die gespeicherte Action auf. Das Grundgerüst dieser Klasse zeigt Listing 3.

Listing 3: Button-Klasse mit Target-Action

class Button: NSObject {
    var target: AnyObject
    var action: Selector
    init(target: AnyObject, action: Selector) {
        self.target = target
        self.action = action
    }
    func performAction() {
        target.performSelectorOnMainThread(action, withObject: self, waitUntilDone: true)
    }
}

Bei target handelt es sich um eine Property vom Typ AnyObject, schließlich kann es sich dabei ja um jedes beliebige Objekt handeln; der genaue Typ braucht uns dabei nicht zu interessieren. action hingegen ist vom Typ Selector. Dieser Typ dient in Swift zur Abbildung von Methodennamen für das Target-Action-Pattern. Der Initializer der Klasse nimmt für beide Properties einen passenden Wert entgegen und weist diese ihnen zu.

Interessant wird es in der Methode performAction. Diese Methode dient beispielhaft für das Betätigen unseres Buttons. Wann immer der Button betätigt wird, soll nun die übergebene Action-Methode auf dem übergebenen Target ausgeführt werden. Zu diesem Zweck steht uns (unter anderem) die Methode performSelectorOnMainThread:withObject:waitUntilDone: der Klasse NSObject zur Verfügung. Die genaue Deklaration dieser Methode habe ich im Folgenden aufgeführt:

func performSelectorOnMainThread(_ aSelector: Selector, withObject arg: AnyObject?, waitUntilDone wait: Bool)

Zunächst einmal erwartet die Methode einen Selector, also den Namen einer Methode, den sie auf dem Objekt, das performSelectorOnMainThread:withObject:waitUntilDone: aufruft, ausführt. Darüber hinaus lässt sich dieser Methode noch ein optionales Objekt übergeben, welches sie an die auszuführende Methode des Target als Parameter mitsendet. Zu guter Letzt legt der finale Parameter wait noch fest, ob die Ausführung des nachfolgenden Codes verhindert werden soll, bis die entsprechende Aktion erfolgreich beendet wurde (true) oder nicht (false).

Wann immer nun also auf einem Button-Objekt die Methode performAction aufgerufen wird, wird die entsprechende Action-Methode des übergebenen Target ausgeführt. Jedes neue Target und jede neue Action sorgt also dafür, das ein Button-Objekt trotz unverändertem Code immer gänzlich verschiedene Aktionen ausführt.

Verwenden von Target-Action

Im Prinzip ist mit der eben gezeigten Implementierung der Button-Klasse alles getan, was zur Vorbereitung von Target-Action nötig ist. Wie nun eine mögliche Verwendung aussehen kann, zeigt Listing 4.

Listing 4: Verwenden von Target-Action

class MyFirstViewController: NSObject {
    func myFirstViewControllerAction(sender: Button) {
        print("MyFirstViewController action.")
    }
}

class MySecondViewController: NSObject {
    func mySecondViewControllerAction(sender: Button) {
        print("MySecondViewController action.")
    }
}

let myFirstViewController = MyFirstViewController()
let mySecondViewController = MySecondViewController()
let firstButton = Button(target: myFirstViewController, action: "myFirstViewControllerAction:")
firstButton.performAction()
// MyFirstViewController action.
let secondButton = Button(target: mySecondViewController, action: "mySecondViewControllerAction:")
secondButton.performAction()
// MySecondViewController action.

Dort werden zwei neue Klassen mit je einer Methode definiert. Der Einfachheit halber geben diese lediglich ein Print-Statement aus. Anschließend werden von diesen beiden Klassen je eine Instanz erstellt und in einer Konstanten gespeichert.

Dann erfolgt die Erstellung zweier Button-Instanzen (firstButton und secondButton). firstButton wird dabei als Target myFirstViewController sowie als Action die einzige Methode der Klasse myFirstViewControllerAction: übergeben, bei secondButton wird äquivalent dazu mySecondViewController mit mySecondViewControllerAction: verwendet. Wie Sie sehen, werden dabei Selectors in Swift als einfache Strings übergeben. Zu beachten ist auch hier der Doppelpunkt am Ende des Methodennamens, da beide Methoden einen Parameter entgegennehmen (nämlich die Instanz der Button-Klasse, die die Methode aufruft).

Je nachdem, auf welche der beiden Button-Instanzen nun die Methode performAction aufgerufen wird, erhält man ein unterschiedliches Ergebnis. Bei firstButton wird die Methode myFirstViewControllerAction: der Klasse MyFirstViewController aufgerufen, bei secondButton hingegen ist es die Methode mySecondViewControllerAction: der Klasse MySecondViewController. Obwohl sich der Code der Klasse Button nicht verändert, führt sie aufgrund des geänderten Targets und der geänderten Action jeweils eine andere Aktion durch; genau das ist das Prinzip und der große Vorteil von Target-Action.

Einsatzgebiete von Target-Action in der iOS-Entwicklung

Einen konkreten Fall für die Verwendung von Target-Action in der iOS-Entwicklung haben wir bereits im letzten Beispiel kennengelernt: Schaltflächen sind ideale Views für den Einsatz von Target-Action, da sie generell nur eine Aktion verstehen (bei Schaltflächen eben das Betätigen derselbigen). So setzt auch die Klasse UIButton aus dem UIKit-Framework auf Target-Action. Andere Beispiele für Target-Action aus dem UIKit-Framework sind die Klassen UISlider oder UISwitch.

Wenn Sie wissen möchten, ob eine bestimmte View-Klasse aus den Frameworks von Apple auf Target-Action setzt, können Sie dazu die Dokumentation von Xcode zu Rate ziehen. Dort wird die Vererbungshierarchie jeder einzelnen Klasse im Feld Inherits from aufgelistet. Findet sich in dieser Auflistung die Klasse UIControl, dann wissen Sie, dass die betreffende Klasse mittels Target-Action konfiguriert werden kann (s.Abb.1).

UIControl stellt dabei einige grundlegende Funktionen zur Arbeit und Verwendung von Target-Action zur Verfügung. Sie können auch selbst eigene View-Klassen auf Basis von UIControl erstellen, um darüber eine Unterstützung für Target-Action zu implementieren. Sie haben aber in diesem Abschnitt gesehen, was genau dahintersteckt und sind in der Lage, selbst eigene Implementierungen für Target-Action zu schreiben und zu erstellen.

Delegation

Bleibt nun noch eine letzte Technik zur Kommunikation zwischen View und Controller, um das Thema "MVC in der iOS-Entwicklung" abzuschließen. Dabei handelt es sich um die sogenannte Delegation. Und der Name ist Programm. Vereinfacht ausgedrückt werden bei der Delegation tatsächlich einzelne Aktionen von einem Objekt an ein anderes "delegiert". Statt also selbst eine Implementierung für eine bestimmte Aktion anzubieten, reicht ein Objekt die Durchführung der entsprechenden Aktion an ein anderes weiter. Je nachdem, wie dieses andere Objekt die gewünschte Aktion implementiert hat, kommt ein anderes Ergebnis zustande, ähnlich wie bei Target-Action.

Damit Delegation funktioniert, braucht es in jedem Fall ein Protokoll, welches die zu delegierenden Methoden definiert. Warum das so ist, erfahren wir gleich. Für den Moment ist aber Fakt, das bei Delegation ohne Protokolle nichts geht; sie sind gewissermaßen die Basis der Delegation.

Möchte nun eine Klasse – beispielsweise eine View – bestimmte Aufgaben an ein anderes Objekt delegieren, so erhält diese Klasse eine Referenz auf das entsprechende Objekt; so weit, so gut, schließlich muss die Klasse ja irgendwie die gewünschten Methoden des Objekts aufrufen können. Doch anstatt für dieses Objekt einen festen statischen Typ anzugeben, wird stattdessen der Typ eines Protokolls verwendet, der festlegt, dass das Objekt konform zum angesprochenen Protokoll sein muss. Das hat zur Folge, das wir zwar nicht wissen, welchen exakten Typ das der Klasse zugewiesene Objekt besitzt, aber wir wissen dafür sehr wohl, dass es die im Protokoll definierten Methoden implementiert und das wir diese somit auf dem Objekt aufrufen können.

Genau das ist der Sinn von Delegation: Wir wissen nichts über den Typ des Objekts, an das wir Aufgaben delegieren, außer, dass es konform zu einem Protokoll ist, welches all die Methoden deklariert, die wir für die Delegation benötigen.

Delegation in der Praxis

Sehen wir uns dazu einmal ein ganz konkretes Beispiel an. Als Vorbild soll die Klasse UITableView aus dem UIKit-Framework dienen, welche ebenfalls mit Delegation arbeitet. Zu diesem Zweck gibt es unter anderem das UITableViewDelegate-Protokoll, welches beispielsweise definiert, was bei Auswahl einer Zelle in der Tabelle geschehen soll.

Listing 5 zeigt die Implementierung eines ähnlichen Beispiels, wenn auch deutlich vereinfacht. Dort werden ein Protokoll ListViewDelegate sowie eine View-Klasse ListView implementiert. Das Protokoll definiert zwei Methoden, die somit jede Klasse implementieren muss, die konform zu ListViewDelegate ist. Die ListView-Klasse selbst verfügt über eine Property namens delegate, der als Typ das Protokoll ListViewDelegate zugewiesen ist. Diese Deklaration hat zur Folge, dass delegate jedes beliebige Objekt zugewiesen werden kann, solange es nur konform zum Protokoll ListViewDelegate ist. Dadurch wissen wir, das wir auf die Property delegate alle Methoden des Protokolls ListViewDelegate aufrufen können. Und genau das geschieht in der Implementierung der Klasse auch an zwei Stellen: Einmal direkt während der Initialisierung, das andere Mal innerhalb einer eigens definierten Methode.

Listing 5: Definition einer Beispiel-View-Klasse mitsamt Delegation-Protokoll

protocol ListViewDelegate {
    func didCreateListView()
    func didSelectRowAtIndex(index: Int)
}

class ListView {
    var delegate: ListViewDelegate
    init(delegate: ListViewDelegate) {
        self.delegate = delegate
        self.delegate.didCreateListView()
    }
    func selectRowAtIndex(index: Int) {
        delegate.didSelectRowAtIndex(index)
    }
}

Damit ist Delegation bereits in ihren Grundzügen umgesetzt, schließlich haben wir nun eine Klasse erstellt, die einzelne Aufrufe an ihr delegate-Objekt weiterreicht. Damit erhält man – ohne den Code von ListView ändern zu müssen – immer ein anderes Ergebnis, abhängig davon, welcher Delegate der Klasse zugewiesen wird. Auch das soll nun anhand eines Beispiels verdeutlicht werden. Listing 6 zeigt dazu die Deklaration zweier beispielhafter Controller-Klassen, die äquivalent zu einem UITableViewController agieren sollen. Beide Klassen sind konform zum ListViewDelegate-Protokoll und müssen daher die beiden im Protokoll deklarierten Methoden implementieren. Die Implementierung unterscheidet sich dabei jeweils bei beiden Klassen.

Listing 6: Implementierung Protokoll-konformer Klassen

class MyFirstListViewController: ListViewDelegate {
    func didCreateListView() {
        print("Create list view with 20 rows.")
    }
    func didSelectRowAtIndex(index: Int) {
        print("MyFirstViewController selected row at index \(index).")
    }
}

class MySecondListViewController: ListViewDelegate {
    func didCreateListView() {
        print("Create list view with 100 rows.")
    }
    func didSelectRowAtIndex(index: Int) {
        print("MySecondViewController selected row at index \(index).")
    }
}

Werden nun Objekte dieser Klassen erzeugt, unterscheidet sich das Ergebnis, das wir aus einem ListView-Objekt heraus erhalten, abhängig davon, welcher Delegate ihm zugewiesen wird. Listing 7 verdeutlicht diese unterschiedlichen Ergebnisse, die abhängig vom gesetzten Delegate zustande kommen.

Listing 7: Zuweisung verschiedener Delegate-Objekte

let myFirstListViewController = MyFirstListViewController()
let mySecondListViewController = MySecondListViewController()
var listView = ListView(delegate: myFirstListViewController)
// Create list view with 20 rows.
listView.selectRowAtIndex(19)
// MyFirstViewController selected row at index 19.
listView = ListView(delegate: mySecondListViewController)
// Create list view with 100 rows.
listView.selectRowAtIndex(99)
// MySecondViewController selected row at index 99.

Vor- und Nachteile von Target-Action und Delegation

Nach der Vorstellung der beiden typischen Techniken zur Kommunikation zwischen View und Controller bleibt auch hier die Frage, wann welche dieser Techniken idealerweise zum Einsatz kommt. Erfreulicherweise lässt sich das relativ schnell und eindeutig beantworten.

Target-Action ist ideal, wenn eine View genau eine bestimmte Aktion ausführen soll, die ein zugeordneter Controller implementieren muss. Dann wird der View einfach das gewünschte Objekt mitsamt auszuführendem Selector zugewiesen und der Aufruf erfolgt dann vonseiten der View, sobald er benötigt wird. Allerdings ist diese Technik eben auch auf genau eine Aktion beschränkt. Soll eine View mehr als eine Aktion an einen Controller weiterreichen, kommt zwangsläufig Delegation ins Spiel.

Delegation ist etwas komplexer als Target-Action, nicht zuletzt aufgrund der Notwendigkeit eines zusätzlichen Protokolls, das die zu delegierenden Aufgaben in Form von Methoden definiert. Dafür hat eine View mit einem solchen Delegate-Objekt die Möglichkeit, deutlich mehr Aktionen an einen Controller weiterzuleiten und von ihm ausführen zu lassen. Gerade komplexe View-Klassen sind daher schnell auf Delegation angewiesen, da sie viele Informationen nicht selbst speichern, sondern dynamisch von einem zugewiesenen Controller abfragen.

Fazit

Das MVC-Pattern ist essenziell wichtig, wenn man Apps für iOS entwickelt. Bereits die System-Frameworks von Apple setzen darauf und es finden sich dort massenhaft Techniken wie Target-Action, Delegation und Notifications; man kommt mit MVC somit selbst bei einfachen Apps sehr schnell in Berührung.

Umso wichtiger ist es, beim Schreiben des eigenen Codes ebenfalls das MVC-Pattern zu beachten und die eigenen Klassen entsprechend zu strukturieren. Stellen Sie sich durchaus während der Erstellung einer Klasse die Frage, in welche der drei Bereiche – Model, View und Controller – die Klasse passt und wo sie hingehört. Wenn Sie darauf keine Antwort wissen, ist Ihre Code-Struktur möglicherweise nicht optimal und sollte noch einmal überdacht werden.

Hat man die verschiedenen Techniken des MVC-Patterns aber einmal verinnerlicht, sorgt die korrekte Anwendung der verschiedenen Funktionen schnell dafür, deutlich besser lesbaren und optimierten Code zu schreiben. Gerade bei der Arbeit im Team werden Ihnen Ihre Kollegen dieses Vorgehen danken. Und auch wenn Sie selbst einmal auf Fehlersuche sind, ist die Strukturierung des eigenen Codes nach MVC eine gute Grundlage, um sich schnell im Projekt zurechtzufinden und die korrekte Problemstelle auszumachen; da es schließlich keine Abhängigkeiten zwischen Model- und View-Klassen gibt, können Sie schneller beurteilen, an welcher Stelle Sie aufgrund des jeweiligen Fehlers ansetzen müssen.

Autor

Thomas Sillmann

Thomas Sillmann ist leidenschaftlicher iOS-App-Entwickler, Trainer und Autor. Freiberuflich tätig programmiert er eigene Apps für den App Store sowie Apps in Form von Kundenaufträgen.
>> Weiterlesen
Das könnte Sie auch interessieren
Kommentare (0)

Neuen Kommentar schreiben