Lektion 1.15 · Stufe 1 – Swift-Fundament

Protokolle

ca. 15 Minuten Lesezeit

Lernziel

Nach dieser Lektion verstehst du, was ein Protokoll ist, kannst eigene Protokolle definieren und erfüllen und kennst die wichtigsten Protokolle der Standardbibliothek: Equatable, Comparable, Hashable, Identifiable und CustomStringConvertible. Und du weißt endlich, was : View bedeutet.

Das Problem: Gemeinsame Fähigkeiten

Du baust eine App für Haustiere. Es gibt Hunde, Katzen und Vögel, jeweils als eigene Struct mit ganz unterschiedlichen Eigenschaften. Eines haben aber alle gemeinsam: Sie haben einen Namen und können ein Geräusch machen.

Jetzt willst du eine Funktion schreiben, die jedes Tier vorstellt:

Swift
func introduce(_ animal: ???) {
    print("\(animal.name) sagt \(animal.sound())")
}

Welchen Typ schreibst du für den Parameter? Dog? Dann funktioniert es nicht für Katzen. Drei verschiedene Funktionen? Das wäre Wiederholung.

Was du brauchst, ist eine Art Vertrag: „Jeder Typ, der einen Namen hat und ein Geräusch machen kann, ist hier willkommen.“ Genau das ist ein Protokoll.

Ein Protokoll definieren

Swift
protocol Animal {
    var name: String { get }
    func sound() -> String
}

Ein Protokoll beschreibt, was ein Typ können muss, aber nicht, wie. Es enthält keine Werte und keinen Code, nur Anforderungen:

  • var name: String { get } heißt: „Es muss eine Eigenschaft name vom Typ String geben, die man lesen kann.“
  • func sound() -> String heißt: „Es muss eine Methode sound() geben, die einen String zurückgibt.“

Ein Bild für Protokolle

Ein Protokoll ist wie eine Stellenausschreibung: „Gesucht: jemand, der Deutsch spricht und einen Führerschein hat.“ Wer die Anforderungen erfüllt, kann sich bewerben, egal wie er sonst ist.

Ein Protokoll erfüllen

Ein Typ übernimmt ein Protokoll mit einem Doppelpunkt nach dem Namen. Man sagt: Er erfüllt das Protokoll (englisch conforms to).

Swift
struct Dog: Animal {
    let name: String
    let breed: String

    func sound() -> String {
        "Wuff!"
    }
}

struct Cat: Animal {
    let name: String
    var livesLeft = 9

    func sound() -> String {
        "Miau!"
    }
}

Beide Structs erfüllen alle Anforderungen von Animal, haben aber zusätzlich ganz eigene Eigenschaften.

Jetzt funktioniert unsere Funktion für jedes Tier:

Swift
func introduce(_ animal: some Animal) {
    print("\(animal.name) sagt \(animal.sound())")
}

introduce(Dog(name: "Bello", breed: "Dackel"))   // Bello sagt Wuff!
introduce(Cat(name: "Minka"))                     // Minka sagt Miau!

Das some Animal heißt: „Irgendein Typ, der Animal erfüllt.“ Genau dieses some kennst du aus some View. Ganz genau erklären wir es in Lektion 1.18.

Was passiert, wenn etwas fehlt?

Swift
struct Bird: Animal {
    let name: String
}

Fehlermeldung

Type 'Bird' does not conform to protocol 'Animal'

Bird verspricht, Animal zu erfüllen, hat aber keine Methode sound(). Xcode bietet ein Fix-it an, das die fehlenden Teile als Platzhalter einfügt.

Aha-Moment: View ist ein Protokoll

Jetzt ergibt die erste Zeile jeder SwiftUI-Datei endlich Sinn:

Swift
struct ContentView: View {
    var body: some View {
        Text("Hallo")
    }
}
  • View ist ein Protokoll von Apple.
  • struct ContentView: View heißt: „ContentView erfüllt das Protokoll View.“
  • Das Protokoll View verlangt genau eine Sache: eine Eigenschaft body, die selbst wieder eine View ist.

Vereinfacht sieht das Protokoll so aus:

Swift
protocol View {
    associatedtype Body: View
    var body: Body { get }
}

Das associatedtype brauchst du jetzt noch nicht zu verstehen. Wichtig ist: Jede View, ob Text, VStack, Button oder deine eigene, erfüllt dieses Protokoll. Deshalb kann man sie beliebig ineinander verschachteln.

Die wichtigsten Protokolle der Standardbibliothek

Swift bringt viele fertige Protokolle mit. Einige wirst du ständig verwenden.

Equatable: Vergleichen mit ==

Swift
struct Point: Equatable {
    var x: Int
    var y: Int
}

let a = Point(x: 1, y: 2)
let b = Point(x: 1, y: 2)
print(a == b)   // true

Das Schöne: Bei Structs, deren Eigenschaften alle selbst Equatable sind, schreibt Swift den Vergleich automatisch für dich. Du musst nur : Equatable dazuschreiben.

Comparable: Vergleichen mit < und Sortieren

Swift
struct Version: Comparable {
    let number: Int

    static func < (lhs: Version, rhs: Version) -> Bool {
        lhs.number < rhs.number
    }
}

let versions = [Version(number: 3), Version(number: 1), Version(number: 2)]
let sorted = versions.sorted()   // 1, 2, 3

Für Comparable musst du selbst festlegen, was „kleiner“ bedeutet. lhs und rhs stehen für left-hand side und right-hand side, die Werte links und rechts vom Operator. Dann funktionieren <, >, sorted(), min() und max() automatisch.

Hashable: Für Sets und Dictionary-Schlüssel

Erinnerst du dich an Lektion 1.7? Damit ein Typ in einem Set oder als Dictionary-Schlüssel verwendet werden kann, muss er Hashable sein:

Swift
struct Tag: Hashable {
    let name: String
}

let tags: Set = [Tag(name: "swift"), Tag(name: "ios"), Tag(name: "swift")]
print(tags.count)   // 2

Auch hier erledigt Swift die Arbeit automatisch, wenn alle Eigenschaften selbst Hashable sind.

Identifiable: Eindeutig erkennbar

Swift
import Foundation

struct TodoItem: Identifiable {
    let id = UUID()
    var title: String
    var isDone = false
}

Identifiable verlangt nur eine Eigenschaft id, die jeden Wert eindeutig kennzeichnet. UUID() erzeugt dafür eine zufällige, weltweit eindeutige Kennung. UUID gehört zum Framework Foundation, deshalb steht das import darüber. (In Playground-Vorlagen und SwiftUI-Dateien ist Foundation oft schon über UIKit oder SwiftUI mit importiert.)

Warum ist das wichtig? Stell dir zwei Aufgaben mit demselben Titel „Einkaufen“ vor. Wie soll eine Liste erkennen, welche gelöscht wurde? Über die id. Genau deshalb verlangen SwiftUI-Listen Identifiable (Lektion 2.16).

CustomStringConvertible: Schöne Ausgabe

Swift
struct Point: CustomStringConvertible {
    var x: Int
    var y: Int

    var description: String {
        "(\(x), \(y))"
    }
}

print(Point(x: 3, y: 4))   // (3, 4)

Damit bestimmst du, wie dein Typ in print und in String-Interpolation erscheint.

Mehrere Protokolle gleichzeitig

Ein Typ kann beliebig viele Protokolle erfüllen. Du trennst sie mit Kommas:

Swift
struct Chore: Identifiable, Hashable, Comparable {
    let id = UUID()
    var title: String
    var priority: Int

    static func < (lhs: Chore, rhs: Chore) -> Bool {
        lhs.priority < rhs.priority
    }
}

Protokolle als Typen in Sammlungen

Du kannst Werte verschiedener Typen in einem Array speichern, solange sie dasselbe Protokoll erfüllen. Dafür schreibst du any vor den Protokollnamen:

Swift
let pets: [any Animal] = [
    Dog(name: "Bello", breed: "Dackel"),
    Cat(name: "Minka")
]

for pet in pets {
    print("\(pet.name): \(pet.sound())")
}

any Animal heißt: „Ein Kasten, in dem irgendein Tier stecken kann, und es darf bei jedem Element ein anderes sein.“ Den Unterschied zwischen some und any schauen wir uns in Lektion 1.18 genauer an.

Typische Fehler

Anforderung fehlt

Fehlermeldung

Type 'Bird' does not conform to protocol 'Animal'

Klick auf den Fehler, und Xcode bietet „Add stubs for conformance“ an. Damit werden alle fehlenden Teile als Platzhalter eingefügt.

Eigenschaft nicht Equatable

Swift
struct Owner { let name: String }

struct Pet: Equatable {
    let name: String
    let owner: Owner
}

Fehlermeldung

Type 'Pet' does not conform to protocol 'Equatable'

Swift kann den Vergleich nur automatisch erzeugen, wenn alle Eigenschaften Equatable sind. Mach Owner auch Equatable.

id fehlt

Fehlermeldung

Type 'TodoItem' does not conform to protocol 'Identifiable'

Identifiable braucht eine Eigenschaft mit genau dem Namen id.

UUID unbekannt

Fehlermeldung

Cannot find 'UUID' in scope

Schreib import Foundation an den Anfang der Datei.

FAQ

Anfänger

Was bedeutet das überhaupt?

Was ist der Unterschied zwischen einem Protokoll und einer Struct? Eine Struct ist ein konkreter Typ mit Daten und Code. Ein Protokoll ist nur eine Beschreibung von Anforderungen. Von einem Protokoll kannst du keine Instanz erzeugen, von einer Struct schon.

Warum heißen manche Protokolle „-able“? Weil sie eine Fähigkeit beschreiben: vergleichbar (Equatable), sortierbar (Comparable), identifizierbar (Identifiable). Das ist eine Swift-Konvention, an die du dich bei eigenen Protokollen gern halten kannst.

Muss ich Equatable und Hashable wirklich selbst hinschreiben? Ja, aber nur den Namen. Den Code dazu erzeugt Swift meist automatisch. Diese automatische Erzeugung nennt man Synthese.

Fortgeschritten

Warum verhält es sich so?

Was bedeutet { get set } in einem Protokoll? { get } heißt: Die Eigenschaft muss lesbar sein. { get set } heißt: Sie muss auch veränderbar sein, also ein var. Eine let-Konstante oder eine berechnete Eigenschaft ohne Setter erfüllt { get set } nicht.

Kann ein Protokoll Code enthalten? Im Protokoll selbst nicht. Mit einer Protocol Extension kannst du aber Standardverhalten für alle Typen bereitstellen, die das Protokoll erfüllen. Das zeigen wir in der nächsten Lektion.

Warum muss ich mutating in Protokollen angeben? Soll eine Methode die Instanz verändern dürfen, steht im Protokoll mutating func .... Structs brauchen das. Klassen ignorieren es einfach.

Profi

Was passiert unter der Haube?

Was ist „protokollorientierte Programmierung“? Apples Ansatz, gemeinsames Verhalten über Protokolle und Protocol Extensions zu teilen statt über Klassenvererbung. Vorteile: Auch Structs und Enums können teilnehmen, ein Typ kann viele Protokolle kombinieren, und es gibt keine starre Hierarchie. SwiftUI ist durch und durch so aufgebaut.

Was sind associatedtypes? Platzhalter für Typen, die erst der erfüllende Typ festlegt. View hat ein associatedtype Body: Jede View bestimmt selbst, welcher konkrete Typ ihr body ist. Protokolle mit associatedtype kann man nicht direkt als Typ verwenden, sondern nur mit some oder any. Genau deshalb steht in SwiftUI some View und nicht einfach View.

Ist any Animal langsamer als some Animal? Ein wenig. any verpackt den Wert in einen Container, in dem der konkrete Typ erst zur Laufzeit feststeht (Existential). some kennt der Compiler beim Übersetzen genau und kann direkt optimieren. In normalem App-Code spielt das kaum eine Rolle, aber es ist ein Grund, some zu bevorzugen, wo es geht.

Übungen

Anfänger

Übung 1: Formen

Definiere ein Protokoll Shape mit einer Eigenschaft area: Double { get }. Baue zwei Structs Rectangle und Circle, die es erfüllen. Die Fläche eines Kreises ist Double.pi * radius * radius.

Lösung Übung 1Erst selbst probieren!
Swift
protocol Shape {
    var area: Double { get }
}

struct Rectangle: Shape {
    let width: Double
    let height: Double
    var area: Double { width * height }
}

struct Circle: Shape {
    let radius: Double
    var area: Double { Double.pi * radius * radius }
}

let shapes: [any Shape] = [Rectangle(width: 3, height: 4), Circle(radius: 1)]
for shape in shapes {
    print(shape.area)
}

Hinweis: In SwiftUI gibt es bereits ein Protokoll Shape sowie Rectangle und Circle. Im Playground ohne import SwiftUI gibt es keinen Konflikt.

Fortgeschritten

Übung 2: Vergleichbare Spieler

Baue eine Struct Player mit name und score, die Comparable und CustomStringConvertible erfüllt. Sortiere ein Array von Spielern mit sorted() und gib die Rangliste aus, z. B. „Anna: 120 Punkte“. Wer mehr Punkte hat, soll weiter vorne stehen.

Lösung Übung 2Erst selbst probieren!
Swift
struct Player: Comparable, CustomStringConvertible {
    let name: String
    let score: Int

    static func < (lhs: Player, rhs: Player) -> Bool {
        lhs.score > rhs.score   // mehr Punkte = "kleiner" = weiter vorne
    }

    var description: String {
        "\(name): \(score) Punkte"
    }
}

let players = [
    Player(name: "Ben", score: 80),
    Player(name: "Anna", score: 120),
    Player(name: "Clara", score: 95)
]

for player in players.sorted() {
    print(player)
}

Der Trick: „Kleiner“ bedeutet hier „gehört weiter nach vorne“. Klarer ist es oft, < natürlich zu definieren und dann sorted(by: >) zu verwenden.

Profi

Übung 3: Eigenes Protokoll

Definiere ein Protokoll Describable mit einer Methode describe() -> String. Lass eine Struct Book, eine Struct Movie und ein Enum Genre es erfüllen. Gib dann ein [any Describable] mit Einträgen aller drei Typen aus.

Lösung Übung 3Erst selbst probieren!
Swift
protocol Describable {
    func describe() -> String
}

enum Genre: String, Describable {
    case fantasy = "Fantasy", drama = "Drama"
    func describe() -> String { "Genre: \(rawValue)" }
}

struct Book: Describable {
    let title: String
    let genre: Genre
    func describe() -> String { "Buch „\(title)“ (\(genre.rawValue))" }
}

struct Movie: Describable {
    let title: String
    let minutes: Int
    func describe() -> String { "Film „\(title)“, \(minutes) Min." }
}

let items: [any Describable] = [
    Book(title: "Momo", genre: .fantasy),
    Movie(title: "Coco", minutes: 105),
    Genre.drama
]

for item in items {
    print(item.describe())
}

Protokolle funktionieren mit Structs, Enums und Klassen gleichermaßen.

Zusammenfassung

  • Ein Protokoll beschreibt Anforderungen: welche Eigenschaften und Methoden ein Typ haben muss.
  • Ein Typ erfüllt ein Protokoll mit : Protokoll. Fehlt etwas, meldet Xcode einen Fehler.
  • View ist ein Protokoll, das genau eine Eigenschaft verlangt: body.
  • Wichtige Standardprotokolle: Equatable (==), Comparable (<, sortieren), Hashable (Sets, Schlüssel), Identifiable (id), CustomStringConvertible (Ausgabe).
  • Swift erzeugt Equatable und Hashable oft automatisch.
  • some Protokoll für „irgendein Typ, der es erfüllt“, any Protokoll für gemischte Sammlungen.

Spickzettel

Swift
protocol Animal {
    var name: String { get }
    func sound() -> String
}
struct Dog: Animal { ... }

struct Item: Identifiable, Hashable {
    let id = UUID()
    var title: String
}

func show(_ a: some Animal) { ... }
let all: [any Animal] = [...]

Lektion geschafft?

Markiere sie als erledigt, damit du deinen Fortschritt siehst.