Lektion 1.18 · Stufe 1 – Swift-Fundament

Generics und some: SwiftUI-Code lesen lernen

ca. 18 Minuten Lesezeit

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:

Swift
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

Swift
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ür T den Typ Int ein.
  • Beim Aufruf mit ["a", "b", "c"] setzt Swift String ein.

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:

KurzschreibweiseAusfü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:

Swift
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: 42

Swift 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:

Swift
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):

Swift
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:

Swift
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:

Swift
VStack {
    Text("Hallo")
    Image(systemName: "star")
}
.padding()

… hat einen Typ, der ungefähr so aussieht:

Swift
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 View erfü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:

Swift
func introduce(_ animal: some Animal) { ... }

Das ist nur eine kürzere Schreibweise für einen Generic:

Swift
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 Animalany Animal
Bedeutungein bestimmter Typ, den der Compiler kenntirgendein Typ, kann sich ändern
Bildeine Kiste mit verdeckter Aufschrift, der Inhalt ist aber festgelegteine Kiste, in die jedes Tier passt
Gemischte Arrays❌ alle Elemente müssen denselben Typ haben✅ [any Animal] mit Hunden und Katzen
Geschwindigkeitoptimaletwas langsamer
Swift
let pet: some Animal = Dog(name: "Bello")       // immer ein Dog
let pets: [any Animal] = [Dog(...), Cat(...)]   // gemischt

Faustregel

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:

Swift
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:

Swift
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:

Swift
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()
    }
}
CodeWas es istLektion
import SwiftUIWerkzeugkasten laden0.3
struct ContentViewein eigener Typ, eine Struct1.12
: Viewerfüllt das Protokoll View1.15
let quotes = [...]eine Konstante, ein Array von Strings1.2, 1.6
@State private vareine Variable, die SwiftUI für uns aufbewahrt2.9
var body: some View { ... }eine berechnete Eigenschaft mit opakem Typ1.12, 1.18
VStack(spacing: 30) { ... }Parameter mit Label und Trailing Closure1.10, 1.11
.font(.title)ein Modifier aus einer Protocol Extension, .title ist ein Enum-artiger Wert1.14, 1.16
Button("…") { ... }Titel plus Closure, die beim Tippen läuft1.11
randomElement() ?? ""Optional mit Ersatzwert1.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

Swift
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

Swift
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

Anfänger

Ü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>
Fortgeschritten

Ü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!
Swift
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.0

T 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].

Profi

Ü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!
Swift
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)        // true

private 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, Set und Optional sind generische Typen: [Int] ist Array<Int>.
  • Einschränkungen wie <T: Equatable> legen fest, was der Platzhalter können muss.
  • some View heiß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 some muss immer denselben Typ liefern. In SwiftUI hilft @ViewBuilder.
  • Du kannst jetzt jede Zeile einer einfachen SwiftUI-View erklären.

Spickzettel

Swift
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

Lektion geschafft?

Markiere sie als erledigt, damit du deinen Fortschritt siehst.