15. Oktober 2018

UIKit: UIView und UIViewController, MVC-Prinzip

View-Hierarchie

UIView-Objekte werden in einer Hierarchie verwaltet. Jedes UIView kann subviews enthalten und kennt sein übergeordnetes View als superview:

View-Hierarchie

Views können aus mit dem Interface Builder erstellten XIB-Dateien oder Storyboards geladen werden oder im Code erzeugt werden. Programmatisch werden UIViews folgendermaßen erstellt und in die Hierarchie eingefügt:

let view = UIView(frame:CGRect(x: 0, y: 0, width: 380, height: 380))
view.backgroundColor = UIColor.white

let innerView = UIView(frame:CGRect(x: 20, y: 80, width: 340, height: 100))
innerView.backgroundColor = UIColor(white: 0.9, alpha: 1.0)

view.addSubview(innerView)

Einem View können weitere Views hinzugefügt werden, beispielsweise ein UILabel und ein UIButton:

let label = UILabel()
label.text = "Hello UIKit!"
label.backgroundColor = UIColor(white: 0.8, alpha: 1.0)
label.sizeToFit()
label.center = CGPoint(x: 170, y: 40)
innerView.addSubview(label)

let button = UIButton(type: .system)
button.frame = CGRect(x: 20, y: 200, width: 340, height: 40)
button.backgroundColor = UIColor.orange
button.setTitle("Button", for: .normal)
view.addSubview(button)

Alle Steuerelemente erben von UIView allgemeine Methoden wie zum Beispiel addSubview: oder removeFromSuperview für die Verwaltung der View-Hierarchie und allgemeine Eigenschaften wie zum Beispiel frame für die Positionierung:

Koordinatenangaben, Frame und Bounds

Für Koordinatenangaben werden die CoreGraphics-Typen CGFloat, CGPoint, CGSize und CGRect verwendet:

let x = CGFloat(0)

let rect = CGRect(x: 0, y: 0, width: 100, height: 100)
let rectZero = CGRect.zero

let point = CGPoint(x: 0, y: 0)
let size = CGSize(width: 100, height: 100)

let upperCenter = CGPoint(x: rect.minX, y:rect.midY)
let smallerRect = rect.insetBy(dx: 1, dy: 1)

NSLog("%@", "\(rect)")

In dem Koordinatensystem eines Views ist CGPoint(x: 0, y: 0) die linke, obere Ecke. Die Positionierung erfolgt über die frame-Eigenschaft relativ zu dem übergeordneten superview (nicht zu verwechseln mit der seltener benötigten Eigenschaft bounds, die Position und Größe des Views in Relation zum eigenen Koordinatensystem angibt):

UIView frame vs. bounds

Alle Koordinatenangaben erfolgen in der Einheit Punkt (pt), die von der Pixeldichte des Displays unabhängig ist. Ein Punkt entspricht dabei unabhängig von der Displayauflösung immer 0,156 mm (bzw. 1/163 inch, ausgehend von der ursprünglichen Displayauflösung des iPhone von 163ppi) und wird automatisch in die entsprechenden Pixelgrößen umgerechnet:

UIColor-Farbangaben

Farben werden mit UIColor-Objekten spezifiziert:

UIColor.red
UIColor(white: 0.5, alpha: 1.0)
UIColor(red:1.0, green:0.0, blue:0.0, alpha:1.0)
UIColor(red:130/255.0, green: 10/255.0, blue: 90/255.0, alpha: 1)

Für das Berechnen von Farbwerten bietet sich der HSV-Farbraum an, da hier sehr einfach Farbvarianten gebildet werden können:

let        color = UIColor(hue: 0.2, saturation: 0.7, brightness: 0.6, alpha: 1.0)
let lighterColor = UIColor(hue: 0.2, saturation: 0.7, brightness: 0.8, alpha: 1.0)

Für das Übernehmen von Farbwerten aus Design-Werkzeugen ist der Swift UIColor Picker praktisch.

UIViewController

Die Klassen des UIKit-Frameworks sind nach dem Model-View-Controller (MVC)-Entwurfsmuster strukturiert. Das Framework stellt neben den Views eine Reihe von Controller-Klassen bereit, die ein View-Hierarchie verwalten:

Übersicht UIKit-Controller

Typischerweise ist ein UIViewController für die Verwaltung eines Screens einer iOS-Anwendung zuständig. Er managt eine Hierarchie von UIViews, koordiniert die Zusammenarbeit mit Modellobjekten und anderen Controllern, passt ggf. Größen und Inhalte der Views an und behandelt UI-Ereignisse:

Das UIView für einen UIViewController kann grafisch mit dem Interface Builder erstellt und zur Laufzeit aus einem Storyboard oder einer XIB-Datei geladen werden oder programmatisch in der loadView-Methode der UIViewController-Implementierung erzeugt werden:

class ExampleViewController: UIViewController {
    override func loadView() {
        self.view = UIView()
    }
}

Darüber hinaus implementiert eine UIViewController-Klasse meist Lebenszyklus-Methoden wie viewDidLoad oder viewWillAppear:, um auf den Controller betreffende Ereignisse zu reagieren und das Verhalten des Controllers zu implementieren:

Zusammenspiel von Model-View-Controller im MVC-Prinzip

Im Folgenden soll das Zusammenspiel zwischen Views, View-Controllern und Modell-Objekten genauer betrachtet werden. Es empfiehlt sich, von der View-Darstellung unabhängige Logik in separaten Modell-Klassen zu implementieren. Hier können zum Beispiel Daten für die Anzeige beschafft werden und gehalten werden. Diese Objekte sind auch ein guter Ort für die Implementierung von Code zur Aufbereitung von Daten, Validierungen, Filterungen, Sortierungen oder Gruppierungen.

Nehmen wir als Beispiel einen PersonsTableViewController, der eine Auswahlliste von Personen bereitstellt:

  1. Der PersonsTableViewController wird ein UITableView für die Anzeige erzeugen und ist dafür zuständig, Anfragen des UITableView nach den Inhalten für die Tabellendarstellung zu beantworten.
  2. Statt die Daten selbst zu verwalten, erzeugt und hält der Controller ein PersonsModel und lässt dieses die Daten beschaffen. Das Modell könnte die Daten von einem Server laden und die Daten aufbereiten, beispielsweise sortieren oder gruppieren.
  3. Der Controller beobachtet das Modell und das Modell löst eine Benachrichtigung aus, sobald die Daten bereitstehen.
  4. Daraufhin wird der Controller das Table-View darüber informieren, dass die View-Darstellung aktualisiert werden muss:

Dabei gilt es zu beachten, dass der Controller das Modell kennt, das Modell hingegen allgemeingültig und vom Controller entkoppelt implementiert wird. Häufig registriert sich daher der Controller beim Modell für Ereignisse und wird benachrichtigt, wenn es Neuigkeiten hinsichtlich des Modells gibt. Dazu werden meist Delegate-Objekte oder CompletionHandler-Blöcke verwendet - diese Mechanismen werden in folgenden Kapiteln behandelt.

Tutorial 1: UIKit-Playground

  1. Erstellen Sie mit File » New » Playground einen neuen Playground mit der Vorlage Single View:

    New Playground: Single View

  2. Instanziieren Sie hier ein UIView mit einer festen Größe, setzen Sie eine Hintergrundfarbe und lassen Sie das View mit Show Results anzeigen:

  3. Machen Sie sich mit dem Code im Playground vertraut und ergänzen Sie ein UIView mit einer Hintergrundfarbe und festen Koordinaten:

    let yellowView = UIView(frame: CGRect(x: 10, y: 10, width: 100, height: 100))
    yellowView.backgroundColor = .yellow
    view.addSubview(yellowView)
  4. Führen Sie den Playground aus und lassen Sie sich das Ergebnis des Live View im Assistant Editor anzeigen:

    UIView im Playground

Tutorial 2: View-Hierarchie debuggen

  1. Starten Sie die App und wählen Sie Debug View Hierarchy:

    Debug View Hierarchy

  2. Die App wird angehalten und Sie können die aktuelle View-Hierarchie inspizieren:

    Baumansicht View Hierarchie

  3. Setzen Sie die Ausführung der App mit Continue fort:

    Debugger: Continue program execution

Tutorial 3: UIViewController-Klasse implementieren

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

  2. Im Storyboard kann für View-Controller eine Klasse konfiguriert werden, die zur Laufzeit diese Ansicht repräsentiert. Öffnen Sie das Storyboard und wählen Sie den Lernkarten-View-Controller aus und prüfen Sie die angegebene Klasse im Identity Inspector ⌘⌥3 - die Klasse ViewController wurde beim Anlegen des Projektes für die initiale Sicht erstellt:

    ViewController-Klasse entfernen
  3. Öffnen Sie die ViewController-Klasse und machen Sie sich mit dieser Klasse vertraut. Sie erbt von der Basisklasse UIViewController aus dem UIKit-Framework. Lassen Sie für diese Klasse via ⌥Klick die Dokumentation anzeigen:

    UIViewController Xcode Docs

  4. Benennen Sie die Klasse via Refactor » Rename zu CardViewController um - dies benennt auch die Quellcodedatei um und passt den Verweis aus dem Storyboard entsprechend an:

    UIViewController Refactor » Rename

  5. Erstellen Sie zur Strukturierung des Projektes per Rechtsklick auf CardViewController mit New Group from Selection eine Gruppe Controller:

    New Group From Selection

  6. Überschreiben Sie mit der Xcode-Code-Vervollständigung ^Leertaste die Methode viewWillAppear, die aufgerufen wird, wenn das View des Controllers auf dem Bildschirm erscheint:

    Xcode: Methoden überschreiben

    Achten Sie dabei darauf, die Methode der super-Klasse aufzufrufen und machen Sie zur Veranschaulichung eine Konsolenausgabe:

    class CardViewController: UIViewController {
    
        override func viewDidLoad() {
            super.viewDidLoad()
            print("viewDidLoad")
        }
    
        override func viewWillAppear(_ animated: Bool) {
            super.viewWillAppear(animated)
    print("viewWillAppear")
        }
    
    }
  7. Setzen Sie dem View in viewDidLoad eine Hintergrundfarbe um die korrekte Einbindung der Controller-Klasse zu testen. Über die Eigenschaft view des Controllers können Sie auf der View des Controllers zugreifen:

    override func viewDidLoad() {
        super.viewDidLoad()
        print("viewDidLoad")
        self.view.backgroundColor = UIColor.yellow
    }
  8. Tipp: Statt der Konstante UIColor.yellow können Sie auch ein Color Literal verwenden, mit dem eine Farbe direkt im Code spezifiziert werden kann:

    Color Literal

    Sie können dies über die Code-Vervollständigung erzeugen:

    Color Literal Code-Vervollständigung

  9. Ergänzen Sie im Controller eine Eigenschaft card für das Modell (die Daten die der Controller anzeigen wird) und verwenden Sie das FlashcardsModel um die erste Lernkarte zu holen. Geben Sie diese im viewDidLoad auf der Konsole aus:

    class CardViewController: UIViewController {
    
        let card = FlashcardsModel.shared.cards[0]
    
        override func viewDidLoad() {
            // ...
            print("Aktuelle Lernkarte:", card.frontText, card.backText)
        }
    
        // ...
    
    }
  10. Starten Sie die App testweise.

  11. Committen Sie die Änderungen als „6.3. CardViewController-Klasse erweitert“.