Lernziel
Nach dieser Lektion verstehst du Generics und kannst Schreibweisen wie Array<Int>, <T> und some View lesen. Du kennst den Unterschied zwischen some und any und kannst damit jede Zeile einer typischen SwiftUI-Datei erklären. Das ist der Abschluss von Stufe 1.
Das Problem: Dieselbe Funktion für jeden Typ
Stell dir eine Funktion vor, die das erste und das letzte Element eines Arrays vertauscht:
func swapEnds(_ array: [Int]) -> [Int] {
guard array.count > 1 else { return array }
var result = array
result.swapAt(0, result.count - 1)
return result
}
print(swapEnds([1, 2, 3])) // [3, 2, 1]Funktioniert, aber nur für [Int]. Für [String] müsstest du die Funktion noch einmal schreiben, mit exakt demselben Code. Für [Double] ein drittes Mal. Für deine eigene [Book] ein viertes Mal.
Dabei interessiert sich die Funktion überhaupt nicht dafür, was im Array steckt. Sie vertauscht nur Positionen. Was du brauchst, ist eine Möglichkeit zu sagen: „Diese Funktion funktioniert für Arrays mit irgendeinem Typ.“ Das sind Generics.
Generics: Platzhalter für Typen
func swapEnds<T>(_ array: [T]) -> [T] {
guard array.count > 1 else { return array }
var result = array
result.swapAt(0, result.count - 1)
return result
}
print(swapEnds([1, 2, 3])) // [3, 2, 1]
print(swapEnds(["a", "b", "c"])) // ["c", "b", "a"]Neu ist nur das <T> nach dem Funktionsnamen. Es bedeutet: „T ist ein Platzhalter für einen Typ, der erst beim Aufruf feststeht.“
- Beim Aufruf mit
[1, 2, 3]setzt Swift fürTden TypIntein. - Beim Aufruf mit
["a", "b", "c"]setzt SwiftStringein.
Die Funktion ist dabei genauso typsicher wie vorher. Gibst du ein [Int] hinein, kommt garantiert ein [Int] heraus, nie ein [String].
Warum T?
T steht für Type und ist reine Konvention. Du kannst auch sprechende Namen verwenden, und Apple tut das oft: Element, Key, Value, Content. Für einfache Fälle ist T aber völlig üblich.
Du benutzt Generics schon die ganze Zeit
Generics klingen neu, aber du verwendest sie seit Lektion 1.6:
| Kurzschreibweise | Ausführliche Schreibweise |
|---|---|
[Int] | Array<Int> |
[String: Int] | Dictionary<String, Int> |
Set<String> | Set<String> |
Int? | Optional<Int> |
Array ist ein generischer Typ. Apple hat ihn einmal geschrieben, mit einem Platzhalter Element für den Inhalt. Du setzt beim Benutzen den konkreten Typ ein. Dasselbe gilt für Dictionary, Set und Optional.
Alle Fähigkeiten, die du kennst, also append, count, first, map, filter, funktionieren deshalb mit jedem Inhaltstyp, auch mit deinen eigenen Structs.
Eigene generische Typen
Auch Structs können Platzhalter haben:
struct Box<Content> {
let content: Content
func describe() -> String {
"Eine Box mit: \(content)"
}
}
let numberBox = Box(content: 42) // Box<Int>
let textBox = Box(content: "Hallo") // Box<String>
print(numberBox.describe()) // Eine Box mit: 42Swift leitet den Typ für Content wieder automatisch ab.
Einschränkungen: Platzhalter mit Bedingungen
Manchmal muss der Platzhalter etwas Bestimmtes können. Diese Funktion soll prüfen, ob ein Wert in einem Array vorkommt:
func appears<T>(_ value: T, in array: [T]) -> Bool {
for item in array {
if item == value { // ❌
return true
}
}
return false
}Fehlermeldung
Binary operator '==' cannot be applied to two 'T' operands
Swift weiß nicht, ob man T überhaupt vergleichen kann. Nicht jeder Typ unterstützt ==. Die Lösung: Du verlangst, dass T das Protokoll Equatable erfüllt (Lektion 1.15):
func appears<T: Equatable>(_ value: T, in array: [T]) -> Bool {
for item in array {
if item == value {
return true
}
}
return false
}
print(appears(3, in: [1, 2, 3])) // true
print(appears("x", in: ["a", "b"])) // false<T: Equatable> heißt: „T darf jeder Typ sein, solange er Equatable erfüllt.“ Hier fügen sich Generics und Protokolle zusammen: Das Protokoll beschreibt, was ein Typ können muss, und der Generic sagt, dass jeder passende Typ willkommen ist.
some: Ein bestimmter, aber verborgener Typ
Jetzt kommen wir zum Wort, das in jeder SwiftUI-Datei steht:
var body: some View {
Text("Hallo")
}Was ist some View?
Erinnerst du dich: View ist ein Protokoll (Lektion 1.15). Jede View hat aber einen ganz konkreten Typ. Text("Hallo") ist vom Typ Text. Und das hier:
VStack {
Text("Hallo")
Image(systemName: "star")
}
.padding()… hat einen Typ, der ungefähr so aussieht:
ModifiedContent<VStack<TupleView<(Text, Image)>>, _PaddingLayout>Niemand will so etwas hinschreiben. Und schlimmer: Jedes Mal, wenn du einen Modifier hinzufügst oder entfernst, würde sich dieser Typ ändern.
some View löst das Problem. Es bedeutet:
„Hier kommt ein bestimmter Typ zurück, der
Viewerfüllt. Welcher genau, rechnet der Compiler selbst aus. Ich muss ihn nicht hinschreiben.“
Der Compiler kennt den genauen Typ trotzdem und kann damit optimal arbeiten. Er wird nur vor dir verborgen. Man nennt das einen opaken Typ (englisch opaque type, undurchsichtiger Typ).
some als Parameter
Dasselbe some kennst du schon aus Lektion 1.15:
func introduce(_ animal: some Animal) { ... }Das ist nur eine kürzere Schreibweise für einen Generic:
func introduce<T: Animal>(_ animal: T) { ... }Beide bedeuten exakt dasselbe: „Irgendein bestimmter Typ, der Animal erfüllt.“
some oder any?
In Lektion 1.15 hast du auch any kennengelernt. Der Unterschied ist subtil, aber wichtig:
some Animal | any Animal | |
|---|---|---|
| Bedeutung | ein bestimmter Typ, den der Compiler kennt | irgendein Typ, kann sich ändern |
| Bild | eine Kiste mit verdeckter Aufschrift, der Inhalt ist aber festgelegt | eine Kiste, in die jedes Tier passt |
| Gemischte Arrays | ❌ alle Elemente müssen denselben Typ haben | ✅ [any Animal] mit Hunden und Katzen |
| Geschwindigkeit | optimal | etwas langsamer |
let pet: some Animal = Dog(name: "Bello") // immer ein Dog
let pets: [any Animal] = [Dog(...), Cat(...)] // gemischtFaustregel
Nimm some, wo es geht. Nimm any, wo du es brauchst, typischerweise bei gemischten Sammlungen.
Warum ein body nur eine Art von View liefern darf
Aus some folgt eine Regel, über die viele Anfänger stolpern:
var body: some View {
if isLoggedIn {
Text("Willkommen")
} else {
Image(systemName: "lock")
}
}Das funktioniert in SwiftUI, obwohl einmal ein Text und einmal ein Image herauskommt. SwiftUI hat hier einen eingebauten Helfer, den View Builder, der beide Fälle in einen gemeinsamen Typ verpackt. In einer normalen Funktion ohne diesen Helfer ginge das nicht:
func makeLabel(isLoggedIn: Bool) -> some View {
if isLoggedIn {
return Text("Willkommen")
} else {
return Image(systemName: "lock")
}
}Fehlermeldung
Function declares an opaque return type 'some View', but the return statements in its body do not have matching underlying types
some View verspricht einen bestimmten Typ, aber hier gäbe es zwei. Die Lösung ist in SwiftUI einfach: das Attribut @ViewBuilder vor die Funktion schreiben, damit SwiftUI wie bei body hilft. Das lernst du in Lektion 2.21.
Die große Auflösung
Jetzt, am Ende von Stufe 1, kannst du jede Zeile dieser Datei erklären:
import SwiftUI
struct ContentView: View {
let quotes = ["Jeder Profi war einmal Anfänger.", "Kleine Schritte sind auch Schritte."]
@State private var currentQuote = "Tippe auf den Button!"
var body: some View {
VStack(spacing: 30) {
Text(currentQuote)
.font(.title)
Button("Neuer Spruch") {
currentQuote = quotes.randomElement() ?? ""
}
}
.padding()
}
}| Code | Was es ist | Lektion |
|---|---|---|
import SwiftUI | Werkzeugkasten laden | 0.3 |
struct ContentView | ein eigener Typ, eine Struct | 1.12 |
: View | erfüllt das Protokoll View | 1.15 |
let quotes = [...] | eine Konstante, ein Array von Strings | 1.2, 1.6 |
@State private var | eine Variable, die SwiftUI für uns aufbewahrt | 2.9 |
var body: some View { ... } | eine berechnete Eigenschaft mit opakem Typ | 1.12, 1.18 |
VStack(spacing: 30) { ... } | Parameter mit Label und Trailing Closure | 1.10, 1.11 |
.font(.title) | ein Modifier aus einer Protocol Extension, .title ist ein Enum-artiger Wert | 1.14, 1.16 |
Button("…") { ... } | Titel plus Closure, die beim Tippen läuft | 1.11 |
randomElement() ?? "" | Optional mit Ersatzwert | 1.8, 1.9 |
Nur @State ist noch offen. Es kommt in Stufe 2.
Geschafft
Herzlichen Glückwunsch! Du hast das Swift-Fundament geschafft. Code, der dir in Lektion 0.4 wie Zauberei vorkam, kannst du jetzt Zeile für Zeile erklären. Im Projekt A wendest du alles an, und dann geht es mit SwiftUI richtig los.
Typische Fehler
Platzhalter ohne Einschränkung
Fehlermeldung
Binary operator '==' cannot be applied to two 'T' operands
Der Generic weiß nicht, was T kann. Ergänze die passende Einschränkung, z. B. <T: Equatable> oder <T: Comparable>.
Verschiedene Typen hinter some
Fehlermeldung
Function declares an opaque return type 'some View', but the return statements in its body do not have matching underlying types
Eine Funktion mit some muss immer denselben konkreten Typ zurückgeben. In SwiftUI hilft @ViewBuilder.
Protokoll mit associatedtype als Typ
import SwiftUI
let screens: [View] = [Text("A"), Image(systemName: "star")]Fehlermeldung
Use of protocol 'View' as a type must be written 'any View'
Protokolle mit associatedtype wie View darfst du nur mit some oder any als Typ verwenden. Bei einfachen Protokollen wie Animal akzeptiert Swift [Animal] zwar noch ohne any. Schreib es trotzdem dazu, denn so sieht jeder sofort, dass hier unterschiedliche Typen drinstecken können.
Typ kann nicht abgeleitet werden
let empty = Box(content: nil)Fehlermeldung
Generic parameter 'Content' could not be inferred
Aus nil allein kann Swift den Typ nicht erkennen. Gib ihn an: Box<String?>(content: nil).
FAQ
Anfänger
Was bedeutet das überhaupt?Muss ich Generics selbst schreiben können?
Am Anfang kaum. Wichtig ist, dass du sie lesen kannst: Array<Int>, <T: Equatable> und vor allem some View. Eigene Generics wirst du in Stufe 3 und 4 schreiben, wenn du wiederverwendbare Komponenten baust.
Ist some View ein Typ wie Int?
Nicht ganz. some View ist eher ein Versprechen: „Hier kommt ein bestimmter View-Typ.“ Der echte Typ steht dahinter, nur verborgen.
Warum ist SwiftUI so kompliziert aufgebaut?
Weil es dadurch extrem schnell ist. Da der Compiler den genauen Typ jeder View kennt, kann SwiftUI sehr effizient erkennen, was sich geändert hat. Die gute Nachricht: Als Entwickler schreibst du einfach some View und musst dich um die Details nicht kümmern.
Fortgeschritten
Warum verhält es sich so?Was bedeutet where bei Generics?
Eine ausführlichere Schreibweise für Einschränkungen: func merge<T>(_ a: [T], _ b: [T]) -> [T] where T: Comparable. Das ist nützlich, wenn die Bedingungen zu lang für die spitzen Klammern werden.
Was ist @ViewBuilder?
Ein Result Builder: ein besonderes Attribut, das mehrere Ausdrücke in einem Block, auch mit if und switch, zu einer einzigen View zusammenbaut. Deshalb kannst du in einem VStack einfach mehrere Views untereinander schreiben, ohne Array und ohne Kommas.
Woran erkenne ich in Apples Dokumentation einen Generic?
An den spitzen Klammern: struct VStack<Content> where Content : View. Lies das als: „VStack hat einen Platzhalter Content, der eine View sein muss.“ Das ist genau der Inhalt, den du in die geschweiften Klammern schreibst.
Profi
Was passiert unter der Haube?Wie setzt der Compiler Generics um?
Je nach Situation auf zwei Arten: durch Spezialisierung, bei der für jeden konkreten Typ eine eigene, optimierte Version erzeugt wird, oder durch gemeinsamen Code, der über versteckte Tabellen mit allen Typen arbeitet. Bei some und klaren Typen kann der Compiler fast immer spezialisieren. Deshalb ist SwiftUI trotz seiner Abstraktionen so schnell.
Was ist der Unterschied zwischen some als Rückgabe und <T>?
Bei <T> bestimmt der Aufrufer den Typ (er gibt z. B. ein [Int] hinein). Bei some als Rückgabetyp bestimmt die Funktion den Typ, und der Aufrufer bekommt ihn nur verdeckt zu sehen. Man spricht deshalb manchmal von „umgekehrten Generics“.
Warum heißt es „type erasure“ bei AnyView?
AnyView verpackt jede beliebige View und löscht ihren konkreten Typ, ähnlich wie any View. Damit kann eine Funktion unterschiedliche Views zurückgeben. Der Preis: SwiftUI kann Änderungen weniger effizient erkennen. Deshalb gilt AnyView als Notlösung. @ViewBuilder ist fast immer besser (Lektion 4.3).
Übungen
Übung 1: Ausführlich schreiben
Schreib die folgenden Typen in der ausführlichen Generic-Schreibweise: [Double], [String: [Int]], Bool?.
Lösung Übung 1Erst selbst probieren!
[Double]→Array<Double>[String: [Int]]→Dictionary<String, Array<Int>>Bool?→Optional<Bool>
Übung 2: Größter Wert
Schreib eine generische Funktion largest(in:), die das größte Element eines Arrays zurückgibt, oder nil bei einem leeren Array. Sie soll mit [Int], [Double] und [String] funktionieren. Verwende nicht max(), sondern eine Schleife. Welche Einschränkung braucht T?
Lösung Übung 2Erst selbst probieren!
func largest<T: Comparable>(in array: [T]) -> T? {
guard var result = array.first else {
return nil
}
for item in array where item > result {
result = item
}
return result
}
print(largest(in: [3, 9, 2]) ?? 0) // 9
print(largest(in: ["Kiwi", "Apfel"]) ?? "") // Kiwi
print(largest(in: [Double]()) ?? -1) // -1.0T muss Comparable sein, sonst kennt Swift kein >. for ... where ist eine Kurzform, um nur passende Elemente zu durchlaufen. [Double]() erzeugt ein leeres Array vom Typ [Double].
Übung 3: Generischer Stapel
Baue eine generische Struct Stack<Element> mit einem privaten Array, einer mutating-Methode push(_:), einer mutating-Methode pop() -> Element?, die das oberste Element entfernt und zurückgibt, und einer berechneten Eigenschaft isEmpty. Teste sie mit Zahlen und mit Strings.
Lösung Übung 3Erst selbst probieren!
struct Stack<Element> {
private var items: [Element] = []
var isEmpty: Bool {
items.isEmpty
}
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element? {
items.popLast()
}
}
var numbers = Stack<Int>()
numbers.push(1)
numbers.push(2)
print(numbers.pop() ?? 0) // 2
var words = Stack<String>()
words.push("Hallo")
print(words.pop() ?? "") // Hallo
print(words.isEmpty) // trueprivate heißt: Von außen kann niemand direkt auf items zugreifen, nur über push und pop. Genau dieses Wort hast du schon bei @State private var gesehen.
Zusammenfassung
- Generics sind Platzhalter für Typen:
func swapEnds<T>(_ array: [T]) -> [T]. Array,Dictionary,SetundOptionalsind generische Typen:[Int]istArray<Int>.- Einschränkungen wie
<T: Equatable>legen fest, was der Platzhalter können muss. some Viewheißt: ein bestimmter View-Typ, den der Compiler kennt, du aber nicht hinschreiben musst.some= ein fester, verborgener Typ.any= irgendein Typ, z. B. in gemischten Arrays.- Eine Funktion mit
somemuss immer denselben Typ liefern. In SwiftUI hilft@ViewBuilder. - Du kannst jetzt jede Zeile einer einfachen SwiftUI-View erklären.
Spickzettel
func first<T>(_ a: [T]) -> T? { a.first }
func contains<T: Equatable>(_ v: T, in a: [T]) -> Bool { ... }
struct Box<Content> { let content: Content }
[Int] == Array<Int>
[String: Int] == Dictionary<String, Int>
Int? == Optional<Int>
var body: some View { ... } // ein verborgener View-Typ
func show(_ a: some Animal) // == func show<T: Animal>(_ a: T)
let all: [any Animal] = [...] // gemischte Sammlung