26. November 2018

Optional-Typen in Swift

Eine Besonderheit bei der Programmierung in Swift ist, dass Variablen standardmäßig nicht ohne Wert (nil) sein dürfen:

// Compilerfehler: Nil cannot initialize specified type
var str : String = nil

Mit einem Fragezeichen wird die Typangabe als optional deklariert:

var str : String?

Dies bewirkt, dass der eigentliche String-Wert in einem Optional<String> verpackt wird:

Mit dem Ausrufezeichen-Operator wird das eigentliche Objekt wieder „ausgepackt“:

str.append(" world")         // Compilerfehler: Value of optional type 'String?' has no member 'append'

str!.append(" world")

Dabei wird von einem gesetzten Wert ausgegangen, andernfalls führt diese Operation zu einem Laufzeitfehler „unexpectedly found nil while unwrapping an Optional value“. Die gegebenenfalls notwendige Prüfung auf nil kann manuell mit einem if-Block erfolgen:

if str != nil {
    str!.append(" world")
}

Diese Operation kann mit dem Fragezeichen-Operator verkürzt geschrieben werden, um die Methode nur dann aufzurufen, wenn ein Wert gesetzt ist:

str?.append(" world")

Eine weitere Möglichkeit der Prüfung auf nil ist eine Zuweisung als Optional Binding mit if let / if var, um auf nil zu prüfen und den eigentlichen Wert auszupacken. Dies ist vor allem dann sinnvoll, wenn mehrere Operationen auf dem Objekt erfolgen sollen:

if var actualStr = str {
    actualStr.append(" ")
    actualStr.append("world")
    actualStr.append("!")
}

Die explizite Deklaration von optionalen Typen hilft bei der frühzeitigen Erkennung von Programmierfehlern. So ist zum Beispiel für Methodenparameter explizit festgelegt, ob nil als Parameterwert erlaubt ist. Der Compiler kann diese Regeln prüfen und entsprechende Fehlermeldungen erzeugen. Zudem wird damit das Typsystem hinsichtlich der Verwendung von primitiven Typen wie Int und Objektreferenzen vereinheitlicht - beides kann ohne Wert sein, sofern die Deklaration entsprechend erfolgt:

var url : URL?
var number : Int?

Tutorial 1: Enum für CardViewController-Zustand verwenden

Im Folgenden soll der Wechsel von Vorder- zur Rückseite der Lernkarte implementiert werden:

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

  2. Öffnen Sie die CardViewController-Klasse und deklarieren Sie ein Enum für die Zustände, in denen sich der Controller befinden kann (Vorder- und Rückseite) sowie eine zugehörige Eigenschaft:

    class CardViewController: UIViewController {
        
        enum Side {
            case front
            case back
        }
        
        var side = Side.front
        
        // ...
        
    }
  3. Extrahieren Sie den Code der viewDidLoad-Methode, der den Text und die Sichtbarkeit der Buttons setzt, mit Editor » Extract Method in eine separate Methode updateView:

    Extract method updateView

  4. Passen Sie die Implementierung von updateView so an, dass der Zustand der side-Eigenschaft berücksichtigt wird:

    fileprivate func updateView() {
        switch(side) {
    case .front: self.textLabel.text = card.frontText
    case .back: self.textLabel.text = card.backText
    }
        self.btnWrong.isHidden = side == .front
        self.btnFlip.isHidden = side == .back
        self.btnCorrect.isHidden = side == .front
    }
  5. Ergänzen Sie in der flip-Methode Code, der mit einem switch-Statement den Wert von side umkehrt und danach die updateView-Methode aufruft um die Darstellung zu aktualisieren.

    @IBAction func flip() {
        switch(side) {
    case .front: self.side = .back
    case .back: self.side = .front
    }
    updateView()
    }
  6. Rufen Sie die flip-Methode auch in den wrong/correct-Methoden auf:

    @IBAction func wrong() {
        print("wrong")
        flip()
    }
    
    @IBAction func correct() {
        print("correct")
        flip()
    }
  7. Testen Sie die App.

Tutorial 2: Optionals

  1. Erstellen Sie mit File » New » Playground einen neuen Playground oder verwenden Sie den Playground aus der Swift-Einführung.

  2. Deklarieren Sie eine Liste mit Wörtern und holen Sie mit der first-Eigenschaft das erste Element der Liste:

    let words = ["bird", "tree", "house"]
    let firstWord = words.first
    print(firstWord)
  3. Verwenden Sie die Xcode-Codevervollständigung um sich den Typ der first-Eigenschaft anzeigen zu lassen sowie -Klick um den Typ von firstWord anzuzeigen:

    Optionaler Rückgabewert von Array.first

    Typ für Variablendeklaration
  4. Rufen Sie eine Methode, z.B. uppercased auf, ohne das Optional zu berücksichtigen:

    print(firstWord.uppercased())

    Sie werden einen Fehler erhalten:

    Playground execution failed: error: MyPlayground.playground:
    error: value of optional type 'String?' not unwrapped; did you mean to use '!' or '?'?
  5. Beheben Sie das Problem, indem Sie den Wert zunächst mit dem !-force unwrap-Operator auspacken und in einer Konstante ablegen:

    let unpackedWord = firstWord!
    print(unpackedWord.uppercased())
  6. Deklarieren Sie die Liste als leere Liste, um das Verhalten zu prüfen, wenn kein Listenelement vorliegt:

    let words : [String] = []

    Sie werden einen fatal error beobachten:

    fatal error: unexpectedly found nil while unwrapping an Optional value
  7. Verwenden Sie zunächst eine gewöhnliche if-Bedingung um diesen Fall zu behandeln:

    let firstWord = words.first
    if firstWord != nil {
        let unpackedWord = firstWord!
        print(unpackedWord.uppercased())
    } else {
        print("Kein Wert")
    }
  8. Verwenden Sie alternativ die Optional Binding-Kurzsyntax if let:

    if let firstWord = words.first {
        print(firstWord.uppercased())
    } else {
        print("Kein Wert")
    }
  9. Verwenden Sie alternativ die Kurzschreibweise mit dem ? Operator, bei dem der folgende Ausdruck nil liefert falls der Wert nil ist:

    print(firstWord?.uppercased())
  10. Verwenden Sie zusätzlich den ??-Operator um einen Fallback-Wert für diesen Fall anzugeben:

    print(firstWord?.uppercased() ?? "Leere Liste")

Tutorial 3: Optional im CardViewController verwenden

  1. Passen Sie die Eigenschaft card im CardViewController so an, dass diese die Eigenschaft first statt den Subscript-Operator [...] verwendet:

    class CardViewController: UIViewController {
    
        // ...
    
        let card = FlashcardsModel.shared.cards.first
        
        // ...
    
    }
  2. Prüfen Sie per Alt-Klick auf die Deklaration das der Typ der Variable damit zu einem Optional wird:

    Optional Type Help
  3. Passen Sie die updateView-Methode so an, dass diese auch funktioniert wenn keine Lernkarte gesetzt ist:

    fileprivate func updateView() {
        switch(side) {
        case .front: self.textLabel.text = card?.frontText
        case .back: self.textLabel.text = card?.backText
        }
        // ...
    }
  4. Übernehmen Sie Ihre Änderungen mit der Commit-Nachricht „9.1. Vorder- und Rückseite für Lernkarte“.