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:
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
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 Eigenschaftnamevom TypStringgeben, die man lesen kann.“func sound() -> Stringheißt: „Es muss eine Methodesound()geben, die einenStringzurü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).
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:
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?
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:
struct ContentView: View {
var body: some View {
Text("Hallo")
}
}Viewist ein Protokoll von Apple.struct ContentView: Viewheißt: „ContentViewerfüllt das ProtokollView.“- Das Protokoll
Viewverlangt genau eine Sache: eine Eigenschaftbody, die selbst wieder eine View ist.
Vereinfacht sieht das Protokoll so aus:
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 ==
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) // trueDas 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
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, 3Fü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:
struct Tag: Hashable {
let name: String
}
let tags: Set = [Tag(name: "swift"), Tag(name: "ios"), Tag(name: "swift")]
print(tags.count) // 2Auch hier erledigt Swift die Arbeit automatisch, wenn alle Eigenschaften selbst Hashable sind.
Identifiable: Eindeutig erkennbar
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
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:
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:
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
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
Ü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!
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.
Ü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!
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.
Ü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!
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. Viewist ein Protokoll, das genau eine Eigenschaft verlangt:body.- Wichtige Standardprotokolle:
Equatable(==),Comparable(<, sortieren),Hashable(Sets, Schlüssel),Identifiable(id),CustomStringConvertible(Ausgabe). - Swift erzeugt
EquatableundHashableoft automatisch. some Protokollfür „irgendein Typ, der es erfüllt“,any Protokollfür gemischte Sammlungen.
Spickzettel
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] = [...]