10. Januar 2019

Closures in Swift

Funktionen als Daten

Swift greift Ideen aus der funktionalen Programmierung auf. So fügen sich Funktionen nahtlos in das Typsystem von Swift ein und können „wie Daten“ verwendet werden. Beispielsweise kann eine Referenz auf eine Funktion in einer Variable abgelegt oder als Parameter übergeben werden:

func triple(value : Int) -> Int {
    return value * 3
}
let operation = triple
operation(10)                              // Rückgabewert 30
func processNumber(value : Int, operation: (Int) -> (Int)) -> Int {
    return operation(value)
}
processNumber(value: 10, operation: triple)       // Rückgabewert 30

Dabei sind die Typen der Parameter und Rückgabewerte entsprechend zu deklarieren:
Mit der Typsignatur (Int) -> (Int) wurde im Beispiel deklariert, dass eine Funktion übergeben werden muss, die einen Int-Wert als Parameter übergeben bekommt und einen Int-Wert zurückliefert.

Closures

Closures.playground Dieses Prinzip wird durch Closures erweitert. Ein Closure ist eine anonym definierte Funktion:

let operation = { (value : Int) -> (Int) in
    return value * 3
}
operation(10)

Die Funktion enthält dabei Zugriff auf ihren Erstellungskontext:

let multiplier = 3
let operation = { (value : Int) -> (Int) in
    return value * multiplier
}
operation(10)

Durch die Verwendung von Werten, die im Kontext einer solchen anonymen Funktion deklariert sind, werden diese an den Block gebunden. Das Binden der benötigten Werte an den Block wird als Capture bezeichnet.

Blockbasierte Listenoperationen

Die Swift Standard Library stellt eine Vielzahl von Methoden bereit, die einen Block als Parameter erwarten. So können die Elemente einer Liste gefiltert werden:

let numbers = [5, 10, 20, 100, 1]

numbers.filter { (value : Int) -> Bool in        // Rückgabewert: [20, 100]
    return value > 10
}

Die Array-Funktion map ruft eine Funktion für jedes Listenelement auf und liefert ein Array mit den zurückgegebenen Werten:

numbers.map(triple)                              // Rückgabewert: [15, 30, 60, 300, 3]
numbers.map({ (value : Int) -> (Int) in
    return value * 3
})

Ebenfalls praktisch ist die Möglichkeit, eine Sortierreihenfolge über einen Block zu definieren:

numbers.sorted(by: { (a : Int, b : Int) -> Bool in
    return a > b
})

Kurzschreibweise für Swift-Blöcke

Neben der oben verwendeten ausführlichen Syntax können Blöcke häufig stark verkürzt geschrieben werden:

let numbers = [5, 10, 20, 100, 1]

numbers.map({(value : Int) -> Int in return value * 3})

Das return-Statement kann entfallen, wenn der Code ein einzelner Ausdruck ist:

numbers.map({(value : Int) -> Int in value * 3})

Die Typen der Parameter / Rückgabewerte werden vom Compiler ermittelt:

numbers.map({(value) in value * 3})

Parameterwerte können ohne explizite Deklaration über $0, $1, $2, ... verwendet werden:

numbers.map({$0 * 3})

Wenn der letzte Parameter einer Funktion ein Block ist, kann der Block hinter den Funktionsaufruf / ohne () geschrieben werden:

numbers.map() {$0 * 3}
numbers.map{$0 * 3}

Blöcke für Ereignisbenachrichtigung

Dieser Mechanismus ist vor allem nützlich für Ereignisbenachrichtigung und nebenläufige Programmierung, da so Code übergeben werden kann, der zu einem späteren Zeitpunkt ausgeführt werden soll, ohne dafür explizit Klassen oder Methoden deklarieren zu müssen.

So könnte ein PersonsModel ein completionHandler-Block übergeben bekommen, der aufgerufen wird, sobald der Ladevorgang abgeschlossen wurde:

class PersonsModel {

    typealias CompletionHander = ([String]) -> ()

    func load(completionHandler : CompletionHander?) {
        // >>> Laden der Daten
        let data = ["Alice", "Bob"]
        // <<<
        completionHandler?(data)
    }

}

(Die Definition eines Typ-Alias im Beispiel verbessert die Lesbarkeit des Codes, ist aber nicht unbedingt notwendig). Der PersonsTableViewController könnte beim Auslösen des Ladevorgangs nun einen Block übergeben, der ausgeführt wird, wenn die Daten bereitstehen:

class PersonsTableViewController : UITableViewController {
    
    let model = PersonsModel()

    func viewDidLoad {
        super.viewDidLoad()
        
        model.load { (persons) in
    // View aktualisieren nachdem die Daten geladen wurden
}
    }

}

Diese Vorgehensweise empfiehlt sich insbesondere für die Benachrichtigung über die Fertigstellung eines Vorganges. Hat eine Modellklasse ein komplexeres Zusammenspiel mit dem Controller, empfiehlt sich die Verwendung eines Delegates, da hier verschiedene Ereignisse über unterschiedliche Methoden im Delegate-Protokoll abgebildet werden können.

Tutorial 1: Array.map verwenden

  1. Laden Sie den Start-Stand von dem Flashcards-Beispielprojekt: Flashcards.zip.

  2. Öffnen Sie StartViewController.swift (Tipp: Open Quickly ⇧⌘O) und verwenden Sie in der downloadFinished-Methode die map-Methode statt einer for ... in-Schleife:

    func downloadFinished(terms: [Term]) {
        let newCards = terms.map { Card(frontText: $0.term, backText: $0.definition) }
    FlashcardsModel.shared.cards.append(contentsOf: newCards)
        self.updateView()
    }
  3. Starten Sie die App und testen Sie den Download eines Kartenstapels. Committen Sie Ihre Änderungen als „17.1. Array.map beim Übernehmen der Downloads“.

Tutorial 2: Animationen mit blockbasierten UIView-Methoden

Im Folgenden soll der Wechsel zwischen Vorder- und Rückseite einer Lernkarte animiert werden:

Im Folgenden soll der Wechsel zwischen Vorder- und Rückseite einer Lernkarte folgendermaßen animiert werden:

Card Flip Animation

  1. Extrahieren Sie die Implementierung von updateView mittels Refactor » Extract Method in CardViewController in drei getrennte Methoden: updateCardLabels, updateButtons und updateNavigationItem.

  2. Animieren Sie in CardViewController.updateButtons das Ein- und Ausblenden des Buttons, so dass die Richtig/Falsch-Buttons aus dem Flip-Button hervorbewegt werden:

    fileprivate func updateButtons() {
        // ...
    
        // Ursprüngliche Position der Buttons sichern
        let posWrong = btnWrong.center
        let posCorrect = btnCorrect.center
        
        // Falsch/Richtig Button in die Mitte über den Flip-Button positionieren...
        btnCorrect.center = btnFlip.center
        btnWrong.center = btnFlip.center
        
        // ...und animiert an die ursprüngliche Position zurückbewegen
        UIView.animate(withDuration: 0.2) {
            self.btnWrong.center = posWrong
            self.btnCorrect.center = posCorrect
        }
    }

    Verwenden Sie dabei die Xcode-Codevervollständigung und bestätigen Sie mit Enter den Vorschlag für den Block-Parameter um den Block-Code zu erzeugen:

    Blöcke: Xcode Completion

  3. Animieren Sie das Setzen des Textes in updateCardLabels mittels UIView.transition(with: ...):

    fileprivate func updateCardLabels() {
        UIView.transition(with: self.textLabel, duration: 0.5, options: .transitionFlipFromTop, animations: {
    
            switch(self.side) {
            case .front: self.textLabel.text = self.card?.frontText
            case .back: self.textLabel.text = self.card?.backText
            }
    
        })
    }
  4. Starten Sie die App testweise und committen Sie Ihre Änderungen als „17.2. CardViewController animiert“.