Skip to content

Swift Spickzettel

Apples Sprache für iOS, macOS und darüber hinaus.

01

Grundlagen

Variablen & Konstanten

Verwenden Sie let standardmäßig für Werte, die sich nicht ändern; wechseln Sie zu var nur, wenn Mutation nötig ist. Swift leitet Typen zur Kompilierzeit ab, aber explizite Annotationen verbessern die Lesbarkeit bei komplexen oder mehrdeutigen Typen.

swift
var name = "Alice"        // mutable
let age = 30               // immutable constant
let pi: Double = 3.14159   // explicit type
var count: Int = 0
count = 10
print(type(of: age))       // Int

Optionals

Optionals repräsentieren die Abwesenheit eines Wertes. Verwenden Sie if-let für sicheres Unwrapping, ?? für Standardwerte und ! nur, wenn Sie sicher sind, dass der Wert existiert (riskant – kann abstürzen).

swift
var nickname: String? = nil
nickname = "Al"
if let n = nickname {
    print("Hi, \(n)")
}
print(nickname ?? "Anonymous")  // nil-coalescing
let force: String = nickname!   // force unwrap (crashes if nil)

Tuples

Tuples gruppieren mehrere Werte zu einem einzigen zusammengesetzten Wert. Nützlich für das Zurückgeben mehrerer Werte aus Funktionen. Benannte Elemente verbessern die Lesbarkeit.

swift
let person = (name: "Alice", age: 30)
print(person.name)   // Alice
print(person.0)      // Alice
let (n, a) = person
print("\(n), \(a)")  // Alice, 30

Typumwandlung

Swift konvertiert nicht implizit zwischen Typen. Verwenden Sie immer explizite Konvertierung (z. B. Double(intVal)), um Mehrdeutigkeit zu vermeiden und subtile Bugs zu verhindern.

swift
let intVal = 42
let doubleVal = Double(intVal)
let strVal = String(intVal)
let fromStr = Int("100")!   // 100
let invalid = Int("abc")    // nil
print(doubleVal, strVal)

Assertions & Vorbedingungen

Verwenden Sie assert für Debugging-Prüfungen (in Release-Builds entfernt) und precondition für kritische Invarianten, die in allen Builds geprüft werden. Beide helfen, Logikfehler früh zu erkennen.

swift
let age = -5
assert(age >= 0, "Age cannot be negative")
precondition(age >= 0, "Age must be non-negative")
// In debug builds, assert crashes if false
// precondition checks in release builds too
02

Strings & Characters

String-Interpolation

String-Interpolation mit \(expr) bettet jeden Ausdruck in einen String ein. Es ist typsicher und wird zur Kompilierzeit ausgewertet, was es sicherer macht als Format-Strings.

swift
let name = "Alice"
let age = 30
let msg = "Name: \(name), Age: \(age)"
let calc = "5 + 3 = \(5 + 3)"
print(msg)        // Name: Alice, Age: 30
print(calc)       // 5 + 3 = 8

Häufige String-Methoden

Swift-Strings sind Unicode-korrekt, das bedeutet, count reflektiert tatsächliche Zeichen (Graphem-Cluster), nicht Bytes. Verwenden Sie diese Methoden statt manueller Indizierung für Sicherheit.

swift
let s = "Hello, World"
print(s.count)              // 12
print(s.uppercased())       // HELLO, WORLD
print(s.lowercased())       // hello, world
print(s.hasPrefix("Hello")) // true
print(s.contains("World"))  // true
print(s.reversed())         // "dlroW ,olleH"

Substring & Indizierung

String-Indizes sind keine Ganzzahlen, da Zeichen unterschiedliche Byte-Größen haben können. Verwenden Sie index(_:offsetBy:) zur Navigation. Substrings teilen Speicher mit dem Original – konvertieren Sie zu String für langfristige Speicherung.

swift
let s = "Hello, World"
let idx = s.index(s.startIndex, offsetBy: 7)
print(s[idx])                     // W
let sub = s[idx...]               // "World"
let newStr = String(sub)          // convert to String
print(s.prefix(5))                // "Hello"

Mehrzeilige Strings

Dreifach zitierte Strings bewahren Zeilenumbrüche und Einrückung. Das schließende """ bestimmt die Basis-Einrückung. Ideal für HTML, JSON oder lange Textblöcke.

swift
let poem = """
Roses are red,
Violets are blue,
Swift is great,
And so are you.
"""
print(poem)
// Use \\(expr) for interpolation in multiline

String-Erstellung & -Splitting

Verwenden Sie joined(separator:), um mit einem Trennzeichen zu verketten, und split(separator:), um zu tokenisieren. Diese sind effizienter als manuelle Schleifen mit +-Verkettung.

swift
let parts = ["apple", "banana", "cherry"]
let joined = parts.joined(separator: ", ")
print(joined)  // apple, banana, cherry
let csv = "a,b,c"
let fields = csv.split(separator: ",")
print(fields)  // ["a", "b", "c"]
let reversed = String(s.reversed())
03

Datenstrukturen

Array

Arrays sind geordnete, null-indizierte Collections. Verwenden Sie append/insert für Hinzufügungen, filter/map/reduce für Transformationen. Bevorzugen Sie Werttypen (Array ist ein struct) für Thread-Sicherheit.

swift
var nums = [1, 2, 3]
nums.append(4)
nums.insert(0, at: 0)
nums[1] = 20
print(nums.count)        // 5
print(nums.contains(20)) // true
let evens = nums.filter { $0 % 2 == 0 }
print(nums.first, nums.last)

Dictionary

Dictionaries speichern Schlüssel-Wert-Paare mit O(1) durchschnittlichem Lookup. Verwenden Sie default:, um nil beim Zugriff auf fehlende Schlüssel zu vermeiden. Schlüssel müssen Hashable sein.

swift
var ages: [String: Int] = ["Alice": 30]
ages["Bob"] = 25
ages["Alice"] = 31
print(ages["Alice", default: 0])  // 31
for (name, age) in ages {
    print("\(name): \(age)")
}
print(ages.keys.sorted())

Set

Sets speichern eindeutige Werte mit O(1)-Mitgliedschaftstest. Ideal für Deduplizierung und Mengenoperationen (Union, Intersection, Difference). Elemente müssen Hashable sein.

swift
var a: Set<Int> = [1, 2, 3]
var b: Set<Int> = [3, 4, 5]
print(a.union(b))           // {1,2,3,4,5}
print(a.intersection(b))    // {3}
print(a.subtracting(b))     // {1,2}
print(a.isSubset(of: b))    // false
a.insert(6)

Ranges

..< ist eine halb-offene Range (schließt obere Grenze aus), ... ist eine geschlossene Range (schließt beide ein). Ranges sind nützlich in Schleifen, Slicing und Pattern Matching mit dem ~=-Operator.

swift
for i in 0..<5 { print(i) }   // 0,1,2,3,4
for i in 0...5 { print(i) }   // 0,1,2,3,4,5
let nums = Array(1...5)       // [1,2,3,4,5]
let prefix = nums.prefix(3)   // [1,2,3]
if 5...10 ~= 7 { print("in range") }

Higher-Order Functions

map transformiert jedes Element, filter wählt passende Elemente aus, reduce kombiniert alle zu einem. Diese sind die Grundlage der funktionalen Programmierung in Swift und ermöglichen prägnante, lesbare Datenpipelines.

swift
let nums = [1, 2, 3, 4, 5]
let doubled = nums.map { $0 * 2 }       // [2,4,6,8,10]
let evens = nums.filter { $0 % 2 == 0 } // [2,4]
let sum = nums.reduce(0, +)             // 15
let strs = nums.map { String($0) }      // ["1","2",...]
print(doubled, evens, sum)
04

Kontrollfluss

If / Else If / Else

Standardmäßige bedingte Verzweigung. Bedingungen müssen boolesch sein (Bool-Typ). Swift erfordert geschweifte Klammern auch bei einzelnen Anweisungen, um Bugs wie Apples goto fail-Schwachstelle zu verhindern.

swift
let score = 85
if score >= 90 {
    print("A")
} else if score >= 80 {
    print("B")
} else if score >= 70 {
    print("C")
} else {
    print("F")
}

Switch (Pattern Matching)

Swift switch ist mächtig: unterstützt Value Binding, Tuples, where-Guards und Ranges. Es muss erschöpfend sein (default deckt verbleibende Fälle ab) und fällt standardmäßig nicht durch.

swift
let point = (2, 0)
switch point {
case (0, 0):
    print("origin")
case (_, 0):
    print("on x-axis")
case (0, _):
    print("on y-axis")
case let (x, y) where x == y:
    print("on diagonal")
default:
    print("elsewhere: \(point.0),\(point.1)")
}

For-In-Schleifen

For-in iteriert über Ranges, Arrays, Dictionaries und jede Sequence. Verwenden Sie enumerated(), wenn Sie den Index benötigen. Verwenden Sie _, um Loop-Variablen zu ignorieren, die Sie nicht verwenden.

swift
for i in 0..<5 { print(i) }
let fruits = ["apple", "banana"]
for (i, fruit) in fruits.enumerated() {
    print("\(i): \(fruit)")
}
let dict = ["a": 1, "b": 2]
for (k, v) in dict { print("\(k)=\(v)") }
for _ in 0..<3 { print("tick") }

While & Repeat-While

while prüft die Bedingung vor jeder Iteration; repeat-while prüft danach (wie do-while in C). Verwenden Sie repeat-while, wenn der Body mindestens einmal ausgeführt werden muss.

swift
var n = 5
while n > 0 {
    print(n)
    n -= 1
}
var x = 0
repeat {
    x += 1
} while x < 3
print(x)  // 3

Guard (frühes Verlassen)

guard bietet frühes Verlassen, wenn Voraussetzungen nicht erfüllt sind. Unwrappte Optionals bleiben im Rest des Scopes verfügbar, was Verschachtelung reduziert. Bevorzugen Sie guard gegenüber tiefem if-let-Nesting.

swift
func greet(_ name: String?) {
    guard let n = name, !n.isEmpty else {
        print("No name provided")
        return
    }
    print("Hello, \(n)")
    // n is unwrapped and available here
}
greet("Alice")
greet(nil)
05

Funktionen & Closures

Funktionsdefinition

Verwenden Sie _, um Argument-Labels für Lesbarkeit wegzulassen. Standardparameterwerte machen Parameter optional. Funktionen mit einem einzelnen Ausdruck haben impliziten Return (Swift 5.9+).

swift
func greet(_ name: String, greeting: String = "Hello") -> String {
    return "\(greeting), \(name)!"
}
print(greet("Alice"))               // Hello, Alice!
print(greet("Bob", greeting: "Hi")) // Hi, Bob!
func add(_ a: Int, _ b: Int) -> Int { a + b }
print(add(3, 4))  // 7

Mehrere Rückgabewerte

Tuples ermöglichen das Zurückgeben mehrerer benannter Werte. Ein optionales Tuple zurückzugeben signalisiert möglichen Fehlschlag. Zugriff via r.min oder r.0 – benannte Elemente sind klarer.

swift
func minMax(_ nums: [Int]) -> (min: Int, max: Int)? {
    guard let first = nums.first else { return nil }
    var mn = first, mx = first
    for n in nums { mn = min(mn, n); mx = max(mx, n) }
    return (mn, mx)
}
if let r = minMax([3, 1, 4, 1, 5]) {
    print("min=\(r.min), max=\(r.max)")
}

Closures

Closures sind eigenständige Funktionalitätsblöcke. Verwenden Sie $0, $1 für Shorthand-Argument-Namen. Trailing-Closure-Syntax (Weglassen des letzten Argument-Labels) ist idiomatisch für map/filter/reduce.

swift
let square: (Int) -> Int = { x in x * x }
print(square(5))  // 25
let add: (Int, Int) -> Int = { $0 + $1 }
print(add(3, 4))  // 7
let nums = [1, 2, 3]
let doubled = nums.map { $0 * 2 }
print(doubled)  // [2, 4, 6]

Escaping & Auto-Closures

@escaping markiert Closures, die gespeichert oder nach Funktionsrückkehr aufgerufen werden (nötig für Async-Callbacks). @autoclosure wickelt einen Ausdruck in eine Closure ein und verzögert dessen Auswertung (verwendet in assert).

swift
var handlers: [() -> Void] = []
func saveHandler(_ fn: @escaping () -> Void) {
    handlers.append(fn)
}
saveHandler { print("done") }
handlers.first?()
// @autoclosure delays evaluation
func logIfTrue(_ cond: @autoclosure () -> Bool) {
    if cond() { print("true") }
}
logIfTrue(2 > 1)

Inout-Parameter

inout-Parameter erlauben Funktionen, die Variable des Aufrufers zu modifizieren (Pass-by-Reference). Verwenden Sie sparsam – bevorzugen Sie das Zurückgeben neuer Werte für Klarheit. Das &-Präfix markiert die Mutationsstelle.

swift
func swap(_ a: inout Int, _ b: inout Int) {
    let temp = a
    a = b
    b = temp
}
var x = 10, y = 20
swap(&x, &y)
print(x, y)  // 20 10
06

Klassen & Structs

Struct (Werttyp)

Structs sind Werttypen – bei Zuweisung kopiert. Verwenden Sie für einfache Datencontainer. Berechnete Properties (distance) berechnen bei Zugriff. mutating-Methoden modifizieren self.

swift
struct Point {
    var x: Double
    var y: Double
    var distance: Double {
        (x * x + y * y).squareRoot()
    }
    mutating func moveBy(dx: Double, dy: Double) {
        x += dx; y += dy
    }
}
var p = Point(x: 3, y: 4)
print(p.distance)  // 5.0

Class (Referenztyp)

Klassen sind Referenztypen – über Referenzen geteilt, unterstützen Vererbung und deinit. Verwenden Sie, wenn Sie Identität, geteilten veränderlichen Zustand oder Objective-C-Interoperabilität benötigen. Andernfalls bevorzugen Sie Structs.

swift
class Person {
    var name: String
    var age: Int
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
    deinit { print("\(name) deallocated") }
}
let p = Person(name: "Alice", age: 30)
print(p.name)  // Alice

Vererbung & Override

Verwenden Sie override, um eine Superclass-Methode neu zu definieren. Swift verwendet dynamischen Dispatch, sodass die Unterklassen-Version aufgerufen wird. Markieren Sie Methoden final, um weiteres Überschreiben für Leistung zu verhindern.

swift
class Animal {
    func speak() { print("...") }
}
class Dog: Animal {
    override func speak() { print("Woof!") }
}
class Cat: Animal {
    override func speak() { print("Meow!") }
}
let pets: [Animal] = [Dog(), Cat()]
for pet in pets { pet.speak() }

Properties (berechnet & lazy)

Berechnete Properties haben get/set-Blöcke; set verwendet standardmäßig newValue. lazy verzögert Initialisierung bis zum ersten Zugriff – nützlich für teure oder selten benötigte Properties. Muss var sein.

swift
class Circle {
    var radius: Double
    var area: Double {
        get { .pi * radius * radius }
        set { radius = (newValue / .pi).squareRoot() }
    }
    lazy var heavy = loadExpensiveData()
    init(radius: Double) { self.radius = radius }
}
func loadExpensiveData() -> String { "data" }

Property-Beobachter

willSet/didSet beobachten Property-Änderungen. Verwenden Sie didSet, um Änderungen zu validieren oder zu reagieren (z. B. Werte clamping, UI-Updates auslösen). Beobachter feuern nicht während init.

swift
class Counter {
    var count: Int = 0 {
        willSet { print("about to set \(newValue)") }
        didSet {
            print("was \(oldValue), now \(count)")
            if count > 10 { count = 10 }
        }
    }
}
let c = Counter()
c.count = 5
c.count = 20  // clamped to 10
07

Protocols & Extensions

Protocol-Definition

Protocols definieren einen Bauplan von Methoden und Properties. Typen konformieren durch Implementierung. Verwenden Sie für Abstraktion, Polymorphismus und Entkopplung – ähnlich Interfaces in Java/C#.

swift
protocol Greetable {
    var name: String { get }
    func greet() -> String
}
struct User: Greetable {
    let name: String
    func greet() -> String { "Hi, \(name)" }
}
let u = User(name: "Alice")
print(u.greet())  // Hi, Alice

Protocol-Extensions (Default-Impl)

Protocol-Extensions bieten Standard-Implementierungen. Dies ermöglicht retroaktive Modellierung und Code-Wiederverwendung ohne Basisklasse. Ein mächtiges Feature zum Teilen von Verhalten über unverwandte Typen hinweg.

swift
protocol Describable {
    func describe() -> String
}
extension Describable {
    func describe() -> String { "A \(type(of: self))" }
}
struct Box: Describable { }
let b = Box()
print(b.describe())  // A Box

Extensions

Extensions fügen vorhandenen Typen Funktionalität hinzu (auch solchen, die Sie nicht besitzen, wie Int). Verwenden Sie, um Code zu organisieren, berechnete Properties hinzuzufügen oder Protocols zu konformieren. Können keine gespeicherten Properties hinzufügen.

swift
extension Int {
    var squared: Int { self * self }
    func times(_ block: () -> Void) {
        for _ in 0..<self { block() }
    }
}
print(5.squared)  // 25
3.times { print("hi") }  // prints hi 3 times

Generics

Generics schreiben flexiblen, wiederverwendbaren Code, der mit jedem Typ arbeitet, während Typsicherheit erhalten bleibt. Verwenden Sie <T> für Typ-Parameter. Constraints (where T: Equatable) schränken erlaubte Typen ein.

swift
func stackOf<T>(_ items: T...) -> [T] {
    var arr: [T] = []
    for item in items { arr.append(item) }
    return arr
}
struct Stack<Element> {
    private var items: [Element] = []
    mutating func push(_ e: Element) { items.append(e) }
}
let s = stackOf(1, 2, 3)  // [Int]

Protocol mit Associated Type

Associated Types lassen Protocols Platzhalter-Typen verwenden, wie Generics für Protocols. Der konformierende Typ gibt den tatsächlichen Typ an. Verwenden Sie typealias, um es explizit zu machen, oder lassen Sie Swift ihn ableiten.

swift
protocol Container {
    associatedtype Item
    var count: Int { get }
    mutating func append(_ item: Item)
    subscript(i: Int) -> Item { get }
}
struct IntStack: Container {
    typealias Item = Int
    private var items: [Int] = []
    var count: Int { items.count }
    mutating func append(_ item: Int) { items.append(item) }
    subscript(i: Int) -> Int { items[i] }
}
08

Fehlerbehandlung

Fehler definieren & werfen

Fehler konformieren zum Error-Protocol (normalerweise Enums). throw signalisiert einen Fehler. Assoziierte Werte (coinsNeeded) tragen Kontext. Verwenden Sie throws, um Funktionen zu markieren, die fehlschlagen können.

swift
enum VendingError: Error {
    case invalidSelection
    case insufficientFunds(coinsNeeded: Int)
    case outOfStock
}
func vend(item: String, coins: Int) throws -> String {
    guard item == "Candy" else { throw VendingError.invalidSelection }
    guard coins >= 2 else { throw VendingError.insufficientFunds(coinsNeeded: 2) }
    return "Dispensing \(item)"
}

Do-Catch & Try

do-catch behandelt geworfene Fehler. try markiert werfende Aufrufe. Pattern-Match auf spezifische Fälle für zielgerichtete Behandlung. Ein Catch-all behandelt unerwartete Fehler. Fehler propagieren den Call-Stack hinauf.

swift
do {
    let result = try vend(item: "Candy", coins: 1)
    print(result)
} catch VendingError.insufficientFunds(let needed) {
    print("Need \(needed) more coins")
} catch VendingError.outOfStock {
    print("Sold out")
} catch {
    print("Other error: \(error)")
}

Try? / Try!

try? konvertiert Fehler zu nil (gibt Optional zurück). try! nimmt Erfolg an und stürzt bei Fehler ab – verwenden Sie nur, wenn Fehlschlag wirklich unmöglich ist. Bevorzugen Sie try? mit Optional-Binding für anmutige Degradation.

swift
let result1 = try? vend(item: "Candy", coins: 1)
print(result1)  // nil (error converted to optional)
let result2 = try! vend(item: "Candy", coins: 5)
print(result2)  // Dispensing Candy (crashes if error)
if let r = try? vend(item: "Candy", coins: 5) {
    print(r)
}

Result-Typ

Result kodiert Erfolg oder Fehlschlag als Wert und ermöglicht Async-Fehlerbehandlung ohne throw. Verwenden Sie für Callbacks, Async-APIs oder wenn Sie Ergebnisse speichern/verketten wollen. .get() wirft bei Fehlschlag.

swift
enum FetchError: Error { case network, parse }
func fetch(_ url: String) -> Result<String, FetchError> {
    url.isEmpty ? .failure(.network) : .success("data from \(url)")
}
switch fetch("https://api.com") {
case .success(let data): print(data)
case .failure(let err): print("Error: \(err)")
}
let value = try? fetch("").get()

Defer (Cleanup)

defer plant Cleanup-Code zur Ausführung, wenn der Scope verlassen wird, unabhängig davon wie (normaler Return, throw, Fehler). Verwenden Sie für Ressourcenfreigabe (Dateien, Locks). Mehrere defers laufen in LIFO-Reihenfolge.

swift
func processFile(_ path: String) throws {
    let handle = openFile(path)
    defer { closeFile(handle) }
    // ... work that might throw ...
    let data = try read(handle)
    // closeFile always runs, even if throw
    print("processed \(data)")
}
09

Datei-I/O & Datum/Zeit

Dateien lesen & schreiben

Foundation bietet einfache Datei-APIs. write(toFile:atomically:) schreibt sicher (Temp-Datei + Umbenennen). Verwenden Sie String(contentsOfFile:) für Text. Für große Dateien verwenden Sie FileHandle für Streaming.

swift
import Foundation
let path = "test.txt"
let content = "Hello, File I/O!"
try content.write(toFile: path, atomically: true,
                  encoding: .utf8)
let read = try String(contentsOfFile: path, encoding: .utf8)
print(read)  // Hello, File I/O!
let lines = read.split(separator: "\n")
print(lines.count)

URL & FileManager

FileManager behandelt Dateisystem-Operationen (erstellen, löschen, Existenz prüfen). Verwenden Sie URLs (nicht Pfade) für moderne APIs. .documentDirectory ist der persistente Speicher der App auf iOS/macOS.

swift
import Foundation
let fm = FileManager.default
let url = fm.urls(for: .documentDirectory, in: .userDomainMask)[0]
    .appendingPathComponent("data.json")
try "data".write(to: url, atomically: true, encoding: .utf8)
let exists = fm.fileExists(atPath: url.path)
print("Exists: \(exists)")
try fm.removeItem(at: url)  // delete

Date & DateFormatter

Date repräsentiert einen Zeitpunkt (intern UTC). DateFormatter konvertiert zwischen Date und String – setzen Sie immer locale auf en_US_POSIX für Fixformat-Parsing, um Locale-Bugs zu vermeiden.

swift
import Foundation
let now = Date()
let fmt = DateFormatter()
fmt.dateFormat = "yyyy-MM-dd HH:mm:ss"
fmt.locale = Locale(identifier: "en_US_POSIX")
let str = fmt.string(from: now)
print(str)  // e.g. 2024-01-15 14:30:00
let parsed = fmt.date(from: "2024-01-01 00:00:00")
print(parsed ?? "invalid")

JSON Encoding/Decoding

Codable automatisiert JSON-Serialisierung. JSONEncoder/JSONDecoder behandeln Konvertierung. Konformieren Sie Ihre Typen zu Codable – der Compiler synthetisiert die Logik. Verwenden Sie CodingKeys, um Schlüsselnamen anzupassen.

swift
struct User: Codable {
    let name: String
    let age: Int
}
let user = User(name: "Alice", age: 30)
let json = try JSONEncoder().encode(user)
let str = String(data: json, encoding: .utf8)!
print(str)  // {"name":"Alice","age":30}
let decoded = try JSONDecoder().decode(User.self, from: json)
print(decoded.name)  // Alice

Datumsberechnungen

Calendar behandelt Datumsarithmetik unter Berücksichtigung von Zeitzonen und DST. Verwenden Sie dateComponents, um Felder zu extrahieren oder Differenzen zu berechnen. Verwenden Sie niemals rohe Sekunden für Datums-Mathematik – verwenden Sie Calendar-APIs.

swift
import Foundation
let cal = Calendar.current
let now = Date()
let tomorrow = cal.date(byAdding: .day, value: 1, to: now)!
let components = cal.dateComponents([.year, .month, .day], from: now)
print(components.year!, components.month!)
let diff = cal.dateComponents([.hour], from: now, to: tomorrow)
print(diff.hour!)  // 24
10

Nebenläufigkeit & Async

Async / Await

async/await (Swift 5.5+) macht Async-Code wie Sync-Code lesbar. Task erstellt einen neuen Async-Kontext. async let läuft nebenläufig und await sammelt alle. Eliminiert Callback-Hölle – bevorzugen Sie gegenüber Completion-Handlern.

swift
func fetchUser(_ id: Int) async -> String {
    try? await Task.sleep(nanoseconds: 1_000_000_000)
    return "User \(id)"
}
Task {
    let user = await fetchUser(42)
    print(user)  // User 42
    async let u1 = fetchUser(1)
    async let u2 = fetchUser(2)
    let users = await [u1, u2]  // concurrent
    print(users)
}

Task & Abbruch

Task repräsentiert eine Einheit Async-Arbeit. Kooperative Abbruch via Task.isCancelled – langlaufende Tasks sollten periodisch prüfen. task.value awaitet das Ergebnis. Abbruch ist kooperativ, nicht erzwungen.

swift
let task = Task {
    for i in 1...100 {
        if Task.isCancelled { return }
        try? await Task.sleep(nanoseconds: 100_000_000)
        print(i)
    }
}
Task {
    try? await Task.sleep(nanoseconds: 500_000_000)
    task.cancel()  // request cancellation
}
await task.value  // wait for completion

Actors (Thread-Sicherheit)

Actors (Swift 5.5+) schützen veränderlichen Zustand vor Data Races durch Serialisierung des Zugriffs. Aller Zugriff geht durch await. Verwenden Sie statt Locks/Queues für geteilten Zustand. Der Compiler verifiziert Sicherheit.

swift
actor Counter {
    private var count = 0
    func increment() { count += 1 }
    func value() -> Int { count }
}
let counter = Counter()
Task {
    await counter.increment()
    print(await counter.value())
}
// Actors serialize access—no data races

GCD (Grand Central Dispatch)

GCD ist die traditionelle Nebenläufigkeits-API. DispatchQueue.global() für Hintergrundarbeit, .main für UI-Updates. QoS (.userInitiated, .background) priorisiert Tasks. Immer noch nützlich für nicht-Async-Code.

swift
import Dispatch
DispatchQueue.global(qos: .userInitiated).async {
    let result = heavyComputation()
    DispatchQueue.main.async {
        print("UI update: \(result)")
    }
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
    print("delayed by 2s")
}

Async Sequence

AsyncSequence ermöglicht Iteration über Async-Werte (wie einen Stream). Verwenden Sie for await zum Konsumieren. Ideal für pagenierte APIs, Server-Sent Events oder jede Quelle, die Werte über Zeit produziert.

swift
let urls = ["url1", "url2", "url3"]
func fetchAll() async {
    for url in urls {
        let data = await fetchData(url)
        print(data)
    }
}
func fetchData(_ url: String) async -> String {
    "data from \(url)"
}
// AsyncSequence: for await item in stream { ... }
11

Protocols Deep Dive

Protocol mit Associated Types

Associated Types (associatedtype) lassen Protocols einen Platzhalter-Typ deklarieren, den konformierende Typen spezifizieren. Dies ist Swifts Äquivalent zu generischen Typ-Parametern für Protocols. Der Typ wird aus den Methoden des konformierenden Typs abgeleitet. Verwenden Sie 'where'-Klauseln, um Associated Types einzuschränken. PATs (Protocols mit Associated Types) können nicht direkt als existenzielle Typen ohne Type Erasure oder (Swift 5.7+) 'any Container' verwendet werden.

swift
protocol Container {
    associatedtype Item
    var count: Int { get }
    mutating func append(_ item: Item)
    subscript(i: Int) -> Item { get }
}

struct IntStack: Container {
    // associatedtype Item inferred as Int
    private var items: [Int] = []
    var count: Int { items.count }
    mutating func append(_ item: Int) { items.append(item) }
    subscript(i: Int) -> Int { items[i] }
}

// Generic constraint with associated type
func sum<C: Container>(_ c: C) -> Int where C.Item == Int {
    var total = 0
    for i in 0..<c.count { total += c[i] }
    return total
}

Protocol-Extensions (Standard-Implementierungen)

Protocol-Extensions bieten Standard-Implementierungen – konformierende Typen erhalten die Methode kostenlos, können sie aber überschreiben. Dies ist Swifts Weg, Typen retroaktiv Funktionalität hinzuzufügen. Eingeschränkte Extensions (where Element: Numeric) fügen Methoden nur Typen hinzu, die die Einschränkung erfüllen. So fügt die Standardbibliothek map/filter/reduce zu allen Collections hinzu.

swift
protocol Describable {
    var description: String { get }
}

// Default implementation via extension
extension Describable {
    var description: String { "A \(type(of: self))" }
}

struct Point: Describable {
    let x, y: Int
    // Uses default description: "A Point"
}

struct Person: Describable {
    let name: String
    var description: String { "Person named (name)" }  // override
}

// Extension with constraints
extension Collection where Element: Numeric {
    var sum: Element { reduce(0, +) }
}
[1, 2, 3].sum  // 6

Protocol-Komposition und Existenzielle

Protocol-Komposition (A & B) erfordert, dass ein Wert zu mehreren Protocols konformiert. Existenzielle Typen (any Protocol) können jeden konformierenden Typ halten, haben aber Laufzeit-Dispatch-Overhead – ein Witness-Table-Lookup pro Aufruf. Verwenden Sie 'some Protocol' (opaker Rückgabetyp), wenn Sie einen spezifischen Typ zurückgeben, aber verbergen wollen. Bevorzugen Sie Generics gegenüber Existenziellen für Leistung; verwenden Sie Existenzielle, wenn Sie heterogene Collections benötigen.

swift
protocol Named { var name: String { get } }
protocol Aged { var age: Int { get } }

struct Person: Named, Aged {
    let name: String
    let age: Int
}

// Protocol composition: requires both protocols
func wishHappyBirthday(to celebrant: Named & Aged) {
    print("Happy birthday, (celebrant.name), you're (celebrant.age)!")
}

let p = Person(name: "Alice", age: 30)
wishHappyBirthday(to: p)

// Existential types (any) — type erasure overhead
let items: [any Named] = [p, Pet(name: "Rex")]
for item in items { print(item.name) }

// Swift 5.7: 'any' keyword required for existentials
// 'some' for opaque types (returns one concrete type)

Protocol-Oriented Programming

Protocol-Oriented Programming ist Swifts Paradigmenwechsel von OOP. Statt Klassenhierarchien um Protocols mit Standard-Implementierungen herum zu designen. Vorteile: funktioniert mit Werttypen (structs/enums), unterstützt retroaktive Konformität (erweitern Sie Typen, die Sie nicht besitzen), ermöglicht mehrfache 'Vererbung' (ein Typ kann zu vielen Protocols konformieren). Klassen sind immer noch nützlich für Referenzsemantik und Objective-C-Interop, aber structs+protocols werden für die meisten Modelle bevorzugt.

swift
// POP: design around protocols, not inheritance
protocol Drawable {
    func draw(on canvas: Canvas)
}

extension Drawable {
    // Default implementation + polymorphism
    func drawTwice(on canvas: Canvas) {
        draw(on: canvas)
        draw(on: canvas)
    }
}

struct Circle: Drawable { let radius: Double
    func draw(on canvas: Canvas) { /* ... */ }
}
struct Square: Drawable { let side: Double
    func draw(on canvas: Canvas) { /* ... */ }
}

// Value types + protocols = no inheritance needed
let shapes: [any Drawable] = [Circle(radius: 5), Square(side: 3)]
shapes.forEach { $0.draw(on: canvas) }

// Retroactive modeling (extend types you don't own)
extension Int: Drawable {
    func draw(on canvas: Canvas) { /* draw the number */ }
}

Benutzerdefinierte Protocol Witnesses

Protocols als Abhängigkeiten (Repository) ermöglichen das Austauschen von Implementierungen (real vs. mock) für Testbarkeit und Flexibilität. Associated Types machen Protocols generisch. Dies ist Swifts Dependency Injection – injizieren Sie einen Protocol-konformierenden Typ. Der 'Protocol Witness' ist der konformierende Typ, der konkretes Verhalten bereitstellt. Dieses Pattern (Repository, Data Source) ist häufig in Swift-Architektur (VIPER, Clean Architecture).

swift
// Protocol with requirements
protocol Repository {
    associatedtype Entity
    func get(_ id: Int) -> Entity?
    func save(_ entity: Entity)
}

// Concrete implementation
struct UserRepo: Repository {
    typealias Entity = User
    func get(_ id: Int) -> User? { /* fetch */ nil }
    func save(_ user: User) { /* persist */ }
}

// Generic function using protocol
func display<R: Repository>(_ repo: R, id: Int) where R.Entity: CustomStringConvertible {
    if let entity = repo.get(id) {
        print(entity.description)
    }
}

// Test double for testing
struct MockUserRepo: Repository {
    typealias Entity = User
    var stored: [Int: User] = [:]
    func get(_ id: Int) -> User? { stored[id] }
    func save(_ user: User) { /* no-op for tests */ }
}
12

Generics Deep Dive

Generische Funktionen und Typen

Generics schreiben flexiblen, wiederverwendbaren Code, der mit jedem Typ arbeitet, während Typsicherheit erhalten bleibt. T ist ein Typ-Parameter (Platzhalter). Der Compiler generiert spezialisierte Versionen für Leistung (kein Boxing). Generische Typen (Stack<T>) bewahren ihren Typ-Parameter. Element ist der generische Typ von Array. Generics werden zur Kompilierzeit aufgelöst – kein Laufzeit-Overhead, im Gegensatz zu Existenziellen.

swift
// Generic function
func swap<T>(_ a: inout T, _ b: inout T) {
    let temp = a; a = b; b = temp
}
var x = 1, y = 2
swap(&x, &y)  // x=2, y=1

// Generic type
struct Stack<T> {
    private var items: [T] = []
    mutating func push(_ item: T) { items.append(item) }
    mutating func pop() -> T? { items.popLast() }
}

var intStack = Stack<Int>()
intStack.push(42)
var strStack = Stack<String>()
strStack.push("hello")

// Generic method on non-generic type
extension Array {
    func chunked(into size: Int) -> [[Element]] {
        stride(from: 0, to: count, by: size).map {
            Array(self[$0..<Swift.min($0 + size, count)])
        }
    }
}

Typ-Einschränkungen

Typ-Einschränkungen limitieren, welche Typen verwendet werden können: T: Equatable erfordert, dass T zu einem Protocol konformiert, T: SomeClass erfordert eine Klassenhierarchie. Die where-Klausel fügt komplexere Einschränkungen hinzu (z. B. übereinstimmende Associated Types). Einschränkungen lassen Sie die Methoden des Protocols verwenden (== für Equatable, < für Comparable). Ohne Einschränkungen können Sie T nur zuweisen und weitergeben – keine Operationen.

swift
// Constraint: T must conform to Equatable
func indexOf<T: Equatable>(_ item: T, in array: [T]) -> Int? {
    for (i, elem) in array.enumerated() {
        if elem == item { return i }  // needs Equatable
    }
    return nil
}

// Multiple constraints
func max<T: Comparable>(_ a: T, _ b: T) -> T {
    return a > b ? a : b
}

// Constraint on associated type
protocol Repository {
    associatedtype Entity: Identifiable
    func find(_ id: Entity.ID) -> Entity?
}

// where clause for complex constraints
func merge<C1: Collection, C2: Collection>(
    _ c1: C1, _ c2: C2
) -> [C1.Element] where C1.Element == C2.Element {
    Array(c1) + Array(c2)
}

Opake Typen (some)

Opake Typen (some Protocol, Swift 5.1) geben einen spezifischen konkreten Typ zurück, der vor dem Aufrufer verborgen ist. Im Gegensatz zu 'any' (existenziell) ist der Typ fixiert und dem Compiler bekannt – kein Boxing, Kompilierzeit-Dispatch. Dies ist die Grundlage von SwiftUIs 'some View'. Verwenden Sie 'some', wenn Sie den konkreten Typ verbergen, aber Leistung erhalten wollen. Der Aufrufer kann Protocol-Methoden verwenden, sich aber nicht auf den spezifischen Typ verlassen.

swift
// Opaque return type: returns ONE concrete type
// (hidden from caller, but fixed per call site)
func makeStack() -> some Collection<Int> {
    return [1, 2, 3]  // concrete type is [Int]
}

let s = makeStack()  // type is 'some Collection<Int>'
print(s.count)  // 3

// Useful for hiding implementation details
protocol Shape { func draw() }
struct Circle: Shape { func draw() { print("circle") } }

func makeShape() -> some Shape {
    return Circle()  // caller doesn't know it's Circle
}

// Difference from existential (any):
// - some: one concrete type, no boxing, compile-time dispatch
// - any: can be different types, runtime dispatch, boxing

// Opaque types in properties
struct View {
    var body: some View { Text("Hello") }  // SwiftUI
}

Bedingte Konformität

Bedingte Konformität macht einen Typ nur dann zu einem Protocol konform, wenn seine Typ-Parameter bestimmte Bedingungen erfüllen. Array<Int> ist Equatable, weil Int es ist; Array<MyStruct> nicht, es sei denn, MyStruct ist es. Dies propagiert: [[Int]] ist Equatable, weil [Int] es ist. Die Standardbibliothek nutzt dies extensiv – Array, Optional, Dictionary sind alle bedingt zu Equatable, Hashable, Codable konform.

swift
// Array conforms to Equatable only if Element is Equatable
extension Array: Equatable where Element: Equatable {
    static func == (lhs: [Element], rhs: [Element]) -> Bool {
        return lhs.count == rhs.count && lhs.elementsEqual(rhs)
    }
}

[1, 2, 3] == [1, 2, 3]  // true (Int: Equatable)
// [SomeStruct] == [SomeStruct]  // error if SomeStruct isn't Equatable

// Conditional conformance for Hashable
extension Stack: Equatable where T: Equatable {
    static func == (lhs: Stack, rhs: Stack) -> Bool {
        return lhs.items == rhs.items
    }
}

// Enables: [Stack<Int>] == [Stack<Int>] (transitively)

Result und generische Fehlerbehandlung

Result<Success, Failure> ist ein generisches Enum für Operationen, die fehlschlagen können, und trägt entweder einen Wert oder einen Fehler. Es ist nützlich, wenn Sie Fehlerbehandlung aufschieben oder Ergebnisse speichern wollen. map transformiert den Erfolgswert; flatMap verkettet Operationen, die auch fehlschlagen können. Result { try ... } konvertiert werfende Funktionen. .get() konvertiert zurück zu werfend. Mit async/await ist Result weniger nötig, aber immer noch nützlich für gespeicherte/weitergegebene Fehler.

swift
// Result: built-in generic for success/failure
enum Result<Success, Failure: Error> {
    case success(Success)
    case failure(Failure)
}

func fetchUser(_ id: Int) -> Result<User, FetchError> {
    if id < 0 { return .failure(.invalidId) }
    return .success(User(id: id))
}

switch fetchUser(42) {
case .success(let user): print(user)
case .failure(let err): print(err)
}

// map/flatMap for chaining
let result = fetchUser(1)
    .map { $0.name }
    .flatMap { name in fetchAvatar(name) }

// Throwing function → Result
let result = Result { try throwingFunction() }
// Result → throwing
let value = try result.get()
13

ARC & Speicherverwaltung

Automatic Reference Counting (ARC)

ARC (Automatic Reference Counting) verwaltet Speicher für Klasseninstanzen (Referenztypen). Jede starke Referenz inkrementiert den Zähler; das Entfernen einer dekrementiert. Wenn der Zähler 0 erreicht, wird das Objekt sofort freigegeben (deterministisch, im Gegensatz zu Garbage Collection). Structs/enums (Werttypen) verwenden kein ARC – sie werden kopiert. ARC kann keine Referenzzyklen behandeln – Sie müssen weak/unowned verwenden, um sie zu brechen.

swift
class Person {
    let name: String
    init(name: String) { self.name = name; print("\(name) initialized") }
    deinit { print("\(name) deinitialized") }
}

var p1: Person? = Person(name: "Alice")  // "Alice initialized"
var p2 = p1  // strong ref count = 2
p1 = nil     // ref count = 1, not deinitialized
p2 = nil     // ref count = 0, "Alice deinitialized"

// ARC tracks strong references:
// - +1 on assignment/copy
// - -1 when variable goes out of scope
// - Object freed when count hits 0
// Unlike GC, deterministic (freed immediately when count=0)

Starke, schwache und unowned Referenzen

Starke Referenzen (Standard) halten Objekte am Leben. weak-Referenzen halten das Objekt nicht am Leben und werden nil, wenn das Objekt freigegeben wird (muss optional var sein). unowned-Referenzen halten das Objekt nicht am Leben, sind aber nicht-optional – verwenden Sie nur, wenn Sie sicher sind, dass das referenzierte Objekt die Referenz überlebt (Absturz, wenn nach Dealloc zugegriffen). Verwenden Sie weak für Delegate-Patterns; unowned für Parent-Child, wo Parent immer Child überlebt.

swift
class Person {
    var name: String
    var apartment: Apartment?  // strong
    init(name: String) { self.name = name }
    deinit { print("\(name) deinit") }
}

class Apartment {
    var unit: String
    weak var tenant: Person?  // weak breaks cycle
    init(unit: String) { self.unit = unit }
    deinit { print("Apartment \(unit) deinit") }
}

var alice: Person? = Person(name: "Alice")
var apt: Apartment? = Apartment(unit: "4A")
alice?.apartment = apt
apt?.tenant = alice  // weak, doesn't keep alice alive

alice = nil  // "Alice deinit" (no strong cycle)
apt = nil    // "Apartment 4A deinit"

// unowned: non-optional, assumed to always have a value
// Use when the referenced object outlives the reference
class Customer {
    var card: CreditCard?  // strong
}
class CreditCard {
    unowned let owner: Customer  // non-nil, non-optional
    init(owner: Customer) { self.owner = owner }
}

Retain-Zyklen und Closures

Closures erfassen Referenzen standardmäßig stark. Wenn eine Klasse eine Closure speichert, die self erfasst, haben Sie einen Retain-Zyklus (beide halten sich gegenseitig am Leben → Speicherleck). Fix mit Capture-Listen: [weak self] (self wird optional, verwenden Sie guard let) oder [unowned self] (nicht-optional, stürzt ab, wenn nil). Verwenden Sie immer weak self in Closures, die als Properties gespeichert oder an langlebige Objekte weitergegeben werden (Beobachter, Async-Tasks).

swift
class ViewModel {
    var name = "VM"
    var onUpdate: (() -> Void)?

    func setup() {
        // BAD: closure captures self strongly → cycle
        onUpdate = {
            print(self.name)  // strong capture
        }
        // ViewModel holds closure, closure holds ViewModel → leak!

        // GOOD: capture list with weak self
        onUpdate = { [weak self] in
            guard let self = self else { return }
            print(self.name)
        }

        // GOOD: unowned if self outlives closure
        onUpdate = { [unowned self] in
            print(self.name)
        }
    }
    deinit { print("VM deinit") }
}

Escaping und Non-escaping Closures

Non-escaping Closures (Standard) laufen innerhalb der Funktion und werden verworfen – kein Retain-Zyklus-Risiko, kein self. nötig in der Closure. @escaping Closures überleben die Funktion (in einer Property gespeichert, async dispatcht usw.) – sie können Retain-Zyklen verursachen, daher müssen Sie self. explizit verwenden und [weak self] in Betracht ziehen. Der Compiler erzwingt dies. Die meisten Completion-Handler sind @escaping; map/filter/reduce sind non-escaping.

swift
class DataLoader {
    var storedHandler: (() -> Void)?

    // @escaping: closure outlives the function (stored or async)
    func load(with handler: @escaping (Data) -> Void) {
        storedHandler = { handler(cachedData) }  // stored
        DispatchQueue.global().async {
            handler(self.fetch())  // escapes via async
        }
    }

    // Non-escaping (default): closure runs during the function
    func process(_ transform: (Int) -> Int) -> [Int] {
        return [1, 2, 3].map(transform)  // runs now, no storage
    }
    // No need for [weak self]—self is alive during the call
}

// @escaping requires explicit self. in closures
class View {
    func load() {
        DataLoader().load { [self] data in  // self. required
            print(data)
        }
    }
}

Speichersicherheit und Exklusivität

Swift erzwingt Exklusivität – eine Variable kann nicht gleichzeitig aus überlappenden Scopes zugegriffen werden (lesen+schreiben oder schreiben+schreiben). Dies verhindert Data Races und undefiniertes Verhalten. inout-Parameter erhalten exklusiven Zugriff für die Aufrufdauer. Struct-mutating-Methoden halten exklusiven Zugriff auf self. Actors (Swift 5.5+) erzwingen Exklusivität auf Sprachebene für geteilten veränderlichen Zustand und eliminieren Data Races per Design.

swift
// Exclusivity: Swift prevents overlapping access to a variable
// This is a compile-time or runtime error:
var step = 1
func increment(_ n: inout Int) {
    step += 1  // accessing global while n is inout to step
}
// increment(&step)  // ERROR: overlapping access

// Fix with explicit copy
func safeIncrement(_ n: inout Int) {
    var copy = step
    copy += 1
    step = copy
}

// Struct mutating methods have exclusive access
struct Counter {
    var count = 0
    mutating func increment(_ other: inout Counter) {
        count += other.count  // ok, different instances
    }
}

// 'inout' parameters: exclusive access for duration
// 'isolation' (Swift 5.5+): actors enforce exclusivity
14

SwiftUI-Grundlagen

Views und Modifier

SwiftUI ist Apples deklaratives UI-Framework. Views sind Structs, die zu View mit einer 'body'-Property konformieren. Modifier (.font, .padding) geben neue gewickelte Views zurück – sie mutieren nicht. Views sind leichte Werttypen; SwiftUI diffed sie, um die tatsächliche UI zu aktualisieren. Der deklarative Stil beschreibt, wie die UI für einen gegebenen Zustand aussehen soll, und SwiftUI behandelt die Übergänge.

swift
import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack(spacing: 20) {
            Text("Hello, World!")
                .font(.title)
                .fontWeight(.bold)
                .foregroundColor(.blue)

            Button(action: { print("tapped") }) {
                HStack {
                    Image(systemName: "star.fill")
                    Text("Tap me")
                }
            }
            .padding()
            .background(Color.orange)
            .cornerRadius(10)
        }
        .padding()
    }
}

// Views are value types (structs), declarative
// Modifiers return new views (immutability)

State und Binding

@State ist für lokalen, veränderlichen View-Zustand – wenn er sich ändert, re-rendered SwiftUI die View. @Binding lässt eine Child-View @State eines Parents über das $-Präfix lesen/schreiben (erstellt ein Binding). State sollte die einzige Quelle der Wahrheit sein; geben Sie Bindings weiter, nicht Werte zum Modifizieren. Für geteilten/komplexen Zustand über Views hinweg verwenden Sie @StateObject/@ObservedObject/@EnvironmentObject mit ObservableObject.

swift
struct CounterView: View {
    @State private var count = 0  // local, mutable state

    var body: some View {
        VStack {
            Text("Count: \(count)")
            Button("Increment") { count += 1 }
        }
    }
}

// @Binding: child receives a reference to parent's state
struct StepperView: View {
    @Binding var value: Int
    var body: some View {
        HStack {
            Button("-") { value -= 1 }
            Text("\(value)")
            Button("+") { value += 1 }
        }
    }
}

// Parent passes binding with $
struct ParentView: View {
    @State var score = 0
    var body: some View {
        StepperView(value: $score)  // $ creates a Binding
    }
}

ObservableObject und EnvironmentObject

ObservableObject (Klasse) ist für geteilten Zustand über mehrere Views hinweg. @Published-Properties lösen View-Updates bei Änderung aus. @StateObject erstellt und besitzt das Objekt (verwenden Sie oben in der Hierarchie). @EnvironmentObject injiziert das Objekt in den View-Baum – jeder Abkömmling kann darauf zugreifen, ohne durch Bindings zu gehen. @ObservedObject empfängt ein vorhandenes Objekt (besitzt es nicht). Verwenden Sie @StateObject für Ownership, @ObservedObject/@EnvironmentObject für Empfangen.

swift
// Model: ObservableObject for shared state
class AppModel: ObservableObject {
    @Published var username: String = ""  // @Published triggers UI update
    @Published var isLoggedIn: Bool = false

    func login() {
        // ... auth logic ...
        isLoggedIn = true
    }
}

// @StateObject: owns the model (created once)
struct RootView: View {
    @StateObject var model = AppModel()
    var body: some View {
        // Inject into environment
        ContentView().environmentObject(model)
    }
}

// @EnvironmentObject: receives from environment
struct ProfileView: View {
    @EnvironmentObject var model: AppModel
    var body: some View {
        Text("Hello, \(model.username)")
    }
}

// @ObservedObject: receives from parent (doesn't own)
struct LoginView: View {
    @ObservedObject var model: AppModel
}

Listen und Navigation

List rendert scrollbare Zeilen (wie UITableView). Elemente müssen zu Identifiable konformieren (oder Sie geben id: an). NavigationStack (iOS 16+) verwaltet Navigation; NavigationLink pusht Ziele. LazyVStack/LazyHStack laden Inhalte verzögert für Leistung. ForEach ist für wiederholte Views innerhalb anderer Container. Verwenden Sie List für tabellarische Daten, ScrollView+LazyVStack für benutzerdefinierte Layouts.

swift
struct Item: Identifiable {
    let id = UUID()
    let name: String
}

struct ListView: View {
    let items = [Item(name: "Apple"), Item(name: "Banana")]

    var body: some View {
        NavigationStack {
            List(items) { item in
                NavigationLink(item.name) {
                    DetailView(item: item)
                }
            }
            .navigationTitle("Fruits")
        }
    }
}

struct DetailView: View {
    let item: Item
    var body: some View {
        Text("Detail: \(item.name)")
            .navigationTitle(item.name)
    }
}

// ForEach for custom layouts
ScrollView {
    LazyVStack {
        ForEach(items) { item in
            Text(item.name)
        }
    }
}

Formulare und Sheets

Form styled automatisch Controls für Einstellungsbildschirme (gruppiert, plattformangemessen). Häufige Controls: TextField, Toggle, Slider, Picker, Stepper. .sheet präsentiert einen Modal; .fullScreenCover für Vollbild. Bindings ($) verbinden Controls mit State. Form passt sich an die Plattform an (iOS gruppierte Liste, macOS Form-Layout). Verwenden Sie Form für Dateneingabe und Einstellungen; VStack für benutzerdefinierte Layouts.

swift
struct SettingsView: View {
    @State private var name = ""
    @State private var notifications = true
    @State private var volume: Double = 0.5
    @State private var theme = "Light"
    @State private var showSheet = false

    var body: some View {
        Form {
            Section("Profile") {
                TextField("Name", text: $name)
                Toggle("Notifications", isOn: $notifications)
            }
            Section("Audio") {
                Slider(value: $volume, in: 0...1)
            }
            Section("Appearance") {
                Picker("Theme", selection: $theme) {
                    Text("Light").tag("Light")
                    Text("Dark").tag("Dark")
                }
            }
            Button("Show Modal") { showSheet = true }
        }
        .sheet(isPresented: $showSheet) {
            Text("Modal content")
        }
    }
}
15

Closures Deep Dive

Closure-Syntax-Variationen

Closures haben viele Syntax-Formen. Volle Form: { (params) -> Type in body }. Typ-Inferenz lässt Sie Typen weglassen. Shorthand-Argumente ($0, $1) ersetzen benannte Parameter. Trailing-Closure-Syntax bewegt die Closure außerhalb (), wenn sie das letzte Argument ist. Mehrere Trailing-Closures (Swift 5.3+) benennen zusätzliche Closures. Verwenden Sie die prägnanteste Form, die noch lesbar ist – Shorthand-Argumente sind toll für kurze Closures wie map.

swift
// Full closure
let add: (Int, Int) -> Int = { (a: Int, b: Int) -> Int in
    return a + b
}

// Type inference
let add2: (Int, Int) -> Int = { a, b in a + b }

// Shorthand argument names ($0, $1...)
let add3: (Int, Int) -> Int = { $0 + $1 }

// Trailing closure syntax
let result = [1, 2, 3].map { $0 * 2 }  // [2, 4, 6]

// Multiple trailing closures (Swift 5.3+)
Button {
    print("action")  // action
} label: {
    Text("Click")    // label
}

// No return for Void closures
let log: (String) -> Void = { msg in print(msg) }

Werte erfassen

Closures erfassen Variablen aus ihrem umschließenden Scope. Standardmäßig erfassen sie per Referenz (Änderungen an der erfassten Variablen sind sichtbar). Capture-Listen [foo] erfassen per Wert (ein Snapshot bei Closure-Erstellung). Für Referenztypen (Klassen) brechen [weak self] oder [unowned self] Retain-Zyklen. Werttypen (structs) per Referenz erfasst sehen immer noch Updates, weil die Closure eine Box hält. Capture-Listen gehen vor die Parameterliste.

swift
// Closures capture surrounding state
func makeIncrementer(amount: Int) -> () -> Int {
    var total = 0
    return {
        total += amount  // captures total and amount
        return total
    }
}

let inc = makeIncrementer(amount: 5)
inc()  // 5
inc()  // 10
// total persists between calls (captured by reference)

// Capture list controls how
class Foo { var x = 0 }
let foo = Foo()
let closure = { [foo] in  // captures foo by value (snapshot)
    print(foo.x)
}
foo.x = 100
closure()  // prints 0 (captured the old value)

// Without capture list, foo is captured by reference
let refClosure = { print(foo.x) }
foo.x = 200
refClosure()  // prints 200

Escaping und Autoclosure

@autoclosure wickelt einen Ausdruck automatisch in eine null-argumentige Closure ein, sodass Aufrufer den Ausdruck direkt schreiben (keine geschweiften Klammern). Dies ermöglicht verzögerte Auswertung – assert() verwendet es, um die Bedingung in Release-Builds zu überspringen. Kombiniert mit @escaping können Sie die Auswertung verzögern. Die &&- und ||-Operatoren verwenden @autoclosure für Kurzschluss-Auswertung. Verwenden Sie @autoclosure sparsam – es verbirgt, dass Code verzögert wird, was Leser verwirren kann.

swift
// @autoclosure: wraps an expression in a closure automatically
func log(_ condition: @autoclosure () -> Bool, _ message: String) {
    if condition() {  // evaluated here, not at call
        print(message)
    }
}
log(2 > 1, "two is greater")  // no { } needed

// assert uses @autoclosure to avoid evaluating in release
assert(isValid, "Invalid state")  // isValid not checked in release

// @autoclosure + @escaping
func deferred(_ block: @autoclosure @escaping () -> Int) {
    DispatchQueue.main.async { print(block()) }
}
deferred(expensiveComputation())  // runs later, on main

// Short-circuit evaluation with @autoclosure
func &&(lhs: @autoclosure () -> Bool, rhs: @autoclosure () -> Bool) -> Bool {
    return lhs() ? rhs() : false  // rhs not evaluated if lhs is false
}

Higher-Order Functions

Swifts Higher-Order Functions (map, filter, reduce, flatMap, compactMap) ermöglichen funktionale Programmierung auf Collections. map transformiert, filter wählt aus, reduce aggregiert, flatMap flacht ab, compactMap entfernt nils. Diese geben neue Collections zurück (Unveränderlichkeit). Chaining komponiert Operationen. Für große Datensätze verwenden Sie .lazy, um intermediäre Arrays zu vermeiden. Dies sind die Grundlage von funktionalem Swift – bevorzugen Sie sie gegenüber imperativen for-Schleifen bei Datentransformation.

swift
let nums = [1, 2, 3, 4, 5, 6]

// map: transform each element
let doubled = nums.map { $0 * 2 }  // [2, 4, 6, 8, 10, 12]

// filter: keep elements matching predicate
let evens = nums.filter { $0 % 2 == 0 }  // [2, 4, 6]

// reduce: combine all into one
let sum = nums.reduce(0, +)  // 21
let product = nums.reduce(1, *)  // 720

// flatMap: transform + flatten
let nested = [[1, 2], [3, 4]]
let flat = nested.flatMap { $0 }  // [1, 2, 3, 4]

// compactMap: transform + filter nils
let strings = ["1", "abc", "3"]
let ints = strings.compactMap { Int($0) }  // [1, 3]

// Chaining (lazy evaluation)
let result = nums
    .filter { $0 % 2 == 0 }
    .map { $0 * $0 }
    .reduce(0, +)  // 4 + 16 + 36 = 56

Closures als Completion-Handler

Completion-Handler waren das Standard-Async-Pattern vor async/await (Swift 5.5). Sie sind @escaping (laufen später) und verwenden oft Result für Fehlerbehandlung. Der Nachteil sind verschachtelte Callbacks ('Pyramide des Verderbens'). async/await macht dies linear und lesbar. Neuer Code sollte async/await verwenden; Completion-Handler bleiben für Delegate-APIs und Objective-C-Interop. Sie können Completion-Handler mit withCheckedContinuation wrappen, um sie mit async/await zu verwenden.

swift
// Completion handler pattern (pre-async/await)
func fetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void) {
    URLSession.shared.dataTask(with: url) { data, _, error in
        if let error = error {
            completion(.failure(error))
            return
        }
        guard let data = data,
              let user = try? JSONDecoder().decode(User.self, from: data) else {
            completion(.failure(DecodeError()))
            return
        }
        completion(.success(user))
    }.resume()
}

// Usage (pyramid of doom)
fetchUser(id: 42) { result in
    switch result {
    case .success(let user):
        fetchAvatar(user) { avatarResult in
            // nested...
        }
    case .failure(let error):
        print(error)
    }
}

// Modern: async/await replaces this
func fetchUserAsync(id: Int) async throws -> User {
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode(User.self, from: data)
}
16

Property Wrappers

Property Wrapper definieren

Property Wrappers (@propertyWrapper) kapseln wiederverwendbares Property-Verhalten. Die wrappedValue-Property ist, womit Benutzer interagieren. Der init empfängt den Anfangswert und alle benutzerdefinierten Argumente. SwiftUI verwendet Property Wrappers extensiv: @State, @Binding, @Published, @AppStorage. Definieren Sie eigene für Validierung, Caching, Logging oder Standardwerte. Der Wrapper ist ein struct/class, der die Speicherung verwaltet.

swift
@propertyWrapper
struct Clamped<Value: Comparable> {
    var value: Value
    let range: ClosedRange<Value>

    var wrappedValue: Value {
        get { value }
        set { value = min(max(newValue, range.lowerBound), range.upperBound) }
    }

    init(wrappedValue: Value, _ range: ClosedRange<Value>) {
        self.value = min(max(wrappedValue, range.lowerBound), range.upperBound)
        self.range = range
    }
}

// Usage
struct Slider {
    @Clamped(0...100) var progress: Int = 50
}
var s = Slider()
s.progress = 150  // clamped to 100
s.progress = -10  // clamped to 0
print(s.progress)  // 0

Projected Value

Die projectedValue (via $ zugegriffen) ist eine sekundäre Schnittstelle zur Property. SwiftUI verwendet dies intensiv: @States $ gibt ein Binding für Child-Views, @Publisheds $ gibt einen Publisher. Definieren Sie projectedValue, wenn der Wrapper etwas über den gewickelten Wert hinaus exponieren soll (ein Binding, einen Publisher, den Wrapper selbst). Die $-Präfix-Syntax macht dies ergonomisch. Nicht alle Wrapper brauchen eine projectedValue – sie ist optional.

swift
@propertyWrapper
struct Observable<T> {
    var value: T
    var wrappedValue: T {
        get { value }
        set { value = newValue; onChange() }
    }
    var onChange: () -> Void = {}

    // projectedValue: accessed via $ (like @State's binding)
    var projectedValue: Observable<T> { self }
}

struct View {
    @Observable var count = 0
    // count: the wrapped value (Int)
    // $count: the projected value (Observable<Int>)
}

// SwiftUI's @State projects a Binding
struct Counter: View {
    @State var value = 0
    var body: some View {
        Stepper("Value: \(value)", value: $value)  // Binding<Int>
    }
}

Eingebaute Property Wrappers

SwiftUI bietet viele Property Wrappers: @State (lokaler Zustand), @Binding (Parent-Zustand), @ObservedObject/@StateObject (externes Modell), @EnvironmentObject (injiziertes Modell), @AppStorage (UserDefaults), @SceneStorage (Scene-Wiederherstellung), @FocusState (Tastaturfokus), @ScaledMetric (Dynamic Type), @Namespace (Animationen). Jeder verwaltet Speicherung und Lebenszyklus. Combines @Published löst UI-Updates aus. Zu wissen, wann man welchen verwendet, ist Schlüssel zu SwiftUI.

swift
import SwiftUI
import Combine

struct ContentView: View {
    @State var count = 0  // local view state

    @Binding var externalValue: Int  // parent-owned

    @EnvironmentObject var model: AppModel  // injected

    @AppStorage("username") var username = ""  // UserDefaults

    @SceneStorage("draft") var draft = ""  // per-scene storage

    @FocusState var isFocused: Bool  // focus management

    @ScaledMetric var size = 16  // Dynamic Type scaling

    @Namespace var animationNamespace  // matched geometry

    // Combine
    @Published var data = [Item]()  // in ObservableObject
}

// @AppStorage auto-syncs with UserDefaults
// @SceneStorage restores state per scene (iPad multitasking)

Benutzerdefinierte Wrapper für Validierung

Property Wrappers glänzen für Cross-Cutting-Concerns. NonEmpty validiert beim Setzen. Cached berechnet verzögert und memoisiert. Andere Ideen: Logged (loggt Änderungen), Trimmed (entfernt Whitespace), Formatted (formatiert bei get), Clamped (Range-Limits), UserDefaults-backed. Wrapper reduzieren Boilerplate – Sie schreiben die Logik einmal und wenden sie mit @ an. Sie komponieren: @Logged @Clamped(0...100) var value. Halten Sie Wrapper auf ein Concern fokussiert.

swift
@propertyWrapper
struct NonEmpty {
    private var value: String = ""
    var wrappedValue: String {
        get { value }
        set {
            guard !newValue.isEmpty else {
                print("Warning: cannot set to empty")
                return
            }
            value = newValue
        }
    }
    init(wrappedValue: String) {
        self.wrappedValue = wrappedValue
    }
}

@propertyWrapper
struct Cached<T> {
    private var cached: T?
    private var generator: () -> T
    var wrappedValue: T {
        mutating get {
            if cached == nil { cached = generator() }
            return cached!
        }
    }
    init(wrappedValue: @autoclosure @escaping () -> T) {
        self.generator = wrappedValue
    }
}

struct Config {
    @NonEmpty var name: String = "default"
    @Cached var expensiveValue: Int = computeExpensive()
}

Property Wrappers in Protocols und Generics

Property Wrappers können generisch sein und in Protocols verwendet werden. Sie haben jedoch Einschränkungen: Ein struct mit gewickelten Properties erhält keine Memberwise-Initialisierer (Sie müssen init manuell schreiben), und Kopieren kann tricky sein (der Wrapper wird kopiert, inklusive seines Zustands). Für Klassen ist dies weniger ein Problem. Trotz Einschränkungen sind Wrapper mächtig für deklaratives, wiederverwendbares Property-Verhalten. SwiftUIs Design verlässt sich schwer auf sie.

swift
// Apply to protocol requirements
protocol Configurable {
    @NonEmpty var name: String { get set }
}

// Generic property wrapper
@propertyWrapper
struct Validated<T> {
    private var value: T?
    let validator: (T) -> Bool
    var wrappedValue: T? {
        get { value }
        set {
            if let v = newValue, validator(v) { value = v }
        }
    }
    init(wrappedValue: T?, validator: @escaping (T) -> Bool) {
        self.validator = validator
        self.wrappedValue = wrappedValue
    }
}

struct Form {
    @Validated(validator: { $0.count >= 8 }) var password: String? = nil
}

// Limitation: property wrappers in structs can't be
// initialized from another instance easily (copy issues).
// They work best as stored properties, not computed.
17

Combine Framework

Publisher und Subscriber

Combine ist Apples reaktives Framework (wie RxSwift). Publisher emitieren Werte; Subscriber empfangen sie. sink erstellt einen Subscriber mit Closures. assign bindet Output an eine Property. Operatoren (map, filter, reduce) transformieren Publisher deklarativ. Combine ist deklarativ – beschreiben Sie die Pipeline, und Werte fließen durch. Es wird für Async-Operationen, UI-Bindings und Event-Handling verwendet. Modernes Swift bevorzugt async/await, aber Combine bleibt für komplexe Pipelines.

swift
import Combine

// Publisher: emits values over time
let publisher = [1, 2, 3].publisher

// Subscriber: receives values
publisher.sink { value in
    print(value)  // 1, 2, 3
}

// With completion
publisher.sink(receiveCompletion: { completion in
    print("done: \(completion)")
}, receiveValue: { value in
    print(value)
})

// Assign to a property
class ViewModel {
    var data: String = ""
}
let vm = ViewModel()
Just("hello").assign(to: &vm.data)

// Operators transform publishers
[1, 2, 3, 4].publisher
    .map { $0 * 2 }
    .filter { $0 > 4 }
    .sink { print($0) }  // 6, 8

@Published und ObservableObject

@Published wickelt eine Property und emitiert ihren neuen Wert an Subscriber, wann immer sie sich ändert. Das $-Präfix greift auf den Publisher zu. Dies überbrückt Combine mit SwiftUI – @Published in einem ObservableObject löst View-Updates aus. Subscriptions werden abgebrochen, wenn der AnyCancellable deallokiert wird, also speichern Sie sie (z. B. in einem Set). Verwenden Sie Combine für Debouncing, Throttling, Kombinieren mehrerer Async-Quellen – Dinge, die async/await weniger elegant behandelt.

swift
class UserStore: ObservableObject {
    @Published var name: String = ""  // publishes on change
    @Published var age: Int = 0

    // $name is a Publisher<String, Never>
    func demo() {
        $name.sink { print("name changed to \($0)") }
        name = "Alice"  // prints "name changed to Alice"
    }
}

// Combine with SwiftUI
struct ProfileView: View {
    @ObservedObject var store: UserStore
    var body: some View {
        Text(store.name)  // updates when name changes
    }
}

// Cancellable: store subscriptions to keep them alive
class Service {
    var cancellables = Set<AnyCancellable>()
    let store = UserStore()

    init {
        store.$name
            .debounce(for: .seconds(0.5), scheduler: RunLoop.main)
            .sink { name in saveToServer(name) }
            .store(in: &cancellables)  // keep alive
    }
}

Operatoren

Combine-Operatoren transformieren, filtern, kombinieren und zeitkontrollieren Publisher. map/scan transformieren; filter/Selektoren wählen; merge/zip/combineLatest kombinieren mehrere Streams; debounce/throttle kontrollieren Timing. Operatoren geben neue Publisher zurück (Unveränderlichkeit, Chaining). debounce wartet auf Ruheperiode (Search-as-you-type); throttle limitiert Rate (Button-Taps). collect sammelt alle Werte in ein Array. Diese ermöglichen deklarative reaktive Pipelines.

swift
let publisher = [1, 2, 3, 4, 5].publisher

// Transforming
publisher.map { $0 * 2 }  // [2, 4, 6, 8, 10]
publisher.scan(0, +)  // [1, 3, 6, 10, 15] (running sum)

// Filtering
publisher.filter { $0 % 2 == 0 }  // [2, 4]
publisher.removeDuplicates()  // consecutive dupes
publisher.first()  // just 1

// Combining
let p1 = [1, 2].publisher
let p2 = [3, 4].publisher
p1.merge(with: p2)  // interleaved: 1, 2, 3, 4 (or any order)
p1.zip(p2)  // pairs: (1,3), (2,4)
p1.combineLatest(p2)  // latest of each

// Timing
publisher.delay(for: 1, scheduler: RunLoop.main)
publisher.throttle(for: 1, scheduler: .main, latest: true)
publisher.debounce(for: 0.5, scheduler: .main)
publisher.collect()  // gather all into [1,2,3,4,5]

Subjects (manuelles Publishing)

Subjects sind veränderliche Publisher, an die Sie manuell Werte senden können – sie überbrücken imperativen und reaktiven Code. PassthroughSubject broadcastet ohne Speicherung (Event-Stream). CurrentValueSubject speichert den neuesten Wert (Zustand). Neue Subscriber zu CurrentValueSubject erhalten sofort den aktuellen Wert. Verwenden Sie Subjects, um Delegates, Notifications oder UI-Events in Combine-Pipelines zu wrappen. @Published ist im Wesentlichen ein CurrentValueSubject, integriert mit ObservableObject.

swift
import Combine

// PassthroughSubject: broadcasts to subscribers, no current value
let subject = PassthroughSubject<Int, Never>()
subject.sink { print($0) }  // subscriber 1
subject.send(1)  // prints 1
subject.send(2)  // prints 2

// CurrentValueSubject: holds current value, new subs get it
let current = CurrentValueSubject<Int, Never>(0)
current.sink { print($0) }  // prints 0 (current)
current.send(1)  // prints 1
current.sink { print($0) }  // prints 1 (current)
current.value  // 1

// Use cases:
// - PassthroughSubject: events (button taps, notifications)
// - CurrentValueSubject: state (like @Published but more control)
// - Bridge imperative code to Combine

// Finish a subject
subject.send(completion: .finished)

Fehlerbehandlung

Combine-Publisher haben einen Failure-Typ (Never für kann-nicht-fehlschlagen). catch ersetzt einen Fehler durch einen Fallback-Publisher. retry abonniert bei Fehlschlag erneut (gut für instabile Netzwerke). mapError transformiert den Fehlertyp. assertNoFailure stürzt ab, wenn ein Fehler auftritt (verwenden Sie, wenn Sie sicher sind). Publisher mit Failure == Never können überall verwendet werden; solche mit Fehlern brauchen Behandlung. Dies macht Fehlerbehandlung in reaktiven Pipelines explizit und komponierbar.

swift
enum APIError: Error { case network, parse }

// Publishers can fail with an error
let publisher = Fail<Int, APIError>(error: .network)

// catch: replace error with fallback publisher
publisher
    .catch { _ in Just(0) }  // fallback to 0
    .sink { print($0) }  // 0

// retry: retry on failure
URLSession.shared.dataTaskPublisher(for: url)
    .retry(3)  // retry up to 3 times
    .sink(receiveCompletion: { _ in }, receiveValue: { _ in })

// mapError: transform error type
publisher
    .mapError { _ in NSError(domain: "x", code: 1) }

// assertNoFailure: crash if error (when you're sure it won't)
Just(1).assertNoFailure().sink { print($0) }

// Result publisher
let resultPublisher = [1, 2, 3].publisher.setFailureType(to: Error.self)
18

Zugriffskontrolle & Code-Organisation

Zugriffsebenen

Swift hat fünf Zugriffsebenen. private (kleinster Scope, innerhalb Deklaration), fileprivate (innerhalb Quelldatei), internal (innerhalb Modul, Standard), public (jeder, der importiert), open (public + subclassbar/overridbar, nur Klassen). Swift 5.9 fügt package für SPM-Module hinzu. Starten Sie restriktiv und weiten Sie bei Bedarf. Verwenden Sie public für Bibliotheks-APIs, internal für App-Code, private für Implementierungsdetails. open ist für Framework-Klassen, die für Subclassing konzipiert sind.

swift
// public: accessible from any module that imports this
public class PublicAPI {
    public func connect() {}
}

// internal (default): accessible within the module
class InternalClass {
    func helper() {}  // internal by default
}

// fileprivate: accessible within the source file
fileprivate func fileHelper() {}

// private: accessible within the enclosing declaration
class Counter {
    private var count = 0  // only Counter can access
    fileprivate func reset() { count = 0 }  // same file
}

// Swift 5.9: package (for Swift Package Manager)
package struct PackageType {
    package var value: Int
}

// Guidelines:
// - Start restrictive (private), open up as needed
// - public for framework API, internal for implementation
// - private for encapsulation within a type

Extensions und Organisation

Extensions lassen Sie die Funktionalität eines Typs über Dateien hinweg aufteilen (String+Validation.swift, String+Parsing.swift) und Konformität retroaktiv hinzufügen. Dies hält Dateien fokussiert und handhabbar. Protocol-Konformität kann in einer separaten Extension/Datei von der Haupttypdefinition sein. Bedingte Konformität (where Element: ...) fügt Konformität bedingt hinzu. Verwenden Sie Extensions zum Organisieren: halten Sie den Kern-Typ minimal, fügen Sie Funktionalität in fokussierten Extension-Dateien hinzu.

swift
// Split a type across files with extensions
// String+Validation.swift
extension String {
    var isValidEmail: Bool {
        contains("@") && contains(".")
    }
}

// String+Parsing.swift
extension String {
    func toURL() -> URL? { URL(string: self) }
}

// Conform to protocols in extensions
extension String: Comparable {  // already conforms, just showing
    // protocol methods
}

// Conditional conformance
extension Array: MyProtocol where Element: MyProtocol {}

// Protocol conformance in a separate file
// (keeps the main type definition clean)
struct User { let id: Int; let name: String }

extension User: Codable {}  // synthesized
extension User: Equatable {}  // synthesized
extension User: CustomStringConvertible {
    var description: String { "User(\(id), \(name))" }
}

Module und Packages

Swift Package Manager (SPM) definiert Module via Package.swift. Jedes Target ist ein Modul – internal-Zugriff ist innerhalb eines Targets, public überquert Module. Dependencies sind andere Packages (git-URLs). Products sind, was Ihr Package exponiert. SPM ist der moderne Weg, Swift-Code zu verwalten (ersetzt CocoaPods/Carthage). Xcode integriert SPM nativ. Struktur: Sources/MyLibrary/ für Code, Tests/MyLibraryTests/ für Tests. Verwenden Sie Module, um Concerns zu trennen und Zugriff zu kontrollieren.

swift
// Package.swift (Swift Package Manager)
// swift-tools-version: 5.9
import PackageDescription

let package = Package(
    name: "MyLibrary",
    products: [
        .library(name: "MyLibrary", targets: ["MyLibrary"]),
    ],
    dependencies: [
        .package(url: "https://github.com/.../Logging", from: "1.0.0"),
    ],
    targets: [
        .target(name: "MyLibrary", dependencies: ["Logging"]),
        .testTarget(name: "MyLibraryTests", dependencies: ["MyLibrary"]),
    ]
)

// Importing modules
import Foundation
import MyLibrary  // your module
import Logging    // dependency

// Each target is a module
// internal access is within a target/module
// public access crosses module boundaries

Initialisierer und Designated/Convenience

Klassen haben Designated-Initialisierer (vollständig init, muss supers Designated aufrufen) und Convenience-Initialisierer (delegieren an einen anderen init in derselben Klasse). Dieses Zwei-Tier-System stellt vollständige Initialisierung sicher. Structs erhalten kostenlos einen Memberwise-Initialisierer. Failable-Initialisierer (init?) geben nil bei Fehlschlag. Required-Initialisierer (required init) müssen von Unterklassen implementiert werden. Für meisten Code bevorzugen Sie Structs (einfachere Initialisierung) gegenüber Klassen.

swift
class Person {
    let name: String
    let age: Int

    // Designated initializer: fully initializes
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

class Employee: Person {
    let employeeId: String

    // Designated init must call super's designated init
    init(name: String, age: Int, employeeId: String) {
        self.employeeId = employeeId
        super.init(name: name, age: age)  // must call super
    }

    // Convenience init must call another init (same class)
    convenience init(name: String) {
        self.init(name: name, age: 0, employeeId: "TEMP")
    }
}

// Structs: memberwise initializer (free)
struct Point { let x, y: Int }
let p = Point(x: 1, y: 2)  // synthesized

// Failable initializers
struct Config {
    let port: Int
    init?(port: Int) {
        guard port > 0 else { return nil }
        self.port = port
    }
}

Optionals Deep Dive

Optionals sind Swifts Null-Sicherheits-Mechanismus – ein Enum, das entweder .some(value) oder .none (nil) ist. Sie müssen unwrappen, um auf den Wert zuzugreifen. Optional-Chaining (?.) navigiert sicher; Nil-Coalescing (??) bietet einen Standard; if let/guard let unwrappen sicher. Vermeiden Sie ! (Force-Unwrap), es sei denn, Sie sind sicher – es stürzt bei nil ab. Optionals zwingen Sie, Abwesenheit explizit zu behandeln, und eliminieren Null-Reference-Exceptions, die in anderen Sprachen häufig sind.

swift
// Optional: enum with .some(Value) and .none
let maybeInt: Int? = 42
let nilInt: Int? = nil

// Optional chaining (short-circuits on nil)
let name: String? = user?.profile?.name  // nil if any link is nil

// Nil-coalescing (default value)
let value = maybeInt ?? 0  // 42
let value2 = nilInt ?? 0   // 0

// Forced unwrapping (crashes if nil)
let forced = maybeInt!  // 42
// let crash = nilInt!   // RUNTIME CRASH

// Optional binding
if let actual = maybeInt {
    print(actual)  // 42, actual is Int (not optional)
}

// Guard for early exit
func process(_ data: Data?) {
    guard let data = data else { return }
    // data is non-optional here
}

// Optional try
let result = try? parse(json)  // nil on error
let result2 = try! parse(json) // crashes on error

// Multiple binding
if let a = optA, let b = optB, a < b { /* ... */ }
19

Speicherverwaltung (ARC/weak)

ARC-Grundlagen

Automatic Reference Counting (ARC) verfolgt starke Referenzen. Wenn der Zähler null erreicht, wird das Objekt deallokiert. ARC ist deterministisch (im Gegensatz zu Garbage Collection): deinit läuft sofort, wenn die letzte Referenz freigegeben wird. Meistens funktioniert ARC einfach. Probleme entstehen mit Referenzzyklen: zwei Objekte referenzieren sich stark gegenseitig und verhindern Deallokation. Verwenden Sie weak oder unowned, um Zyklen zu brechen.

swift
class Person {
    let name: String
    init(name: String) {
        self.name = name
        print("\(name) is being initialized")
    }
    deinit {
        print("\(name) is being deinitialized")
    }
}

var reference1: Person?
var reference2: Person?

reference1 = Person(name: "Alice")
// Prints: Alice is being initialized

reference2 = reference1  // Strong reference count: 2

reference1 = nil  // Count: 1
reference2 = nil  // Count: 0, deinit runs
// Prints: Alice is being deinitialized

Schwache Referenzen

weak-Referenzen halten das referenzierte Objekt nicht am Leben. Wenn das Objekt deallokiert wird, werden weak-Referenzen automatisch nil. weak muss optional sein (var tenant: Person?). Verwenden Sie weak, wenn das referenzierte Objekt eine kürzere Lebensdauer hat (Mieter kann die Wohnung verlassen). Der klassische Anwendungsfall ist das Delegate-Pattern: der Delegate ist weak, um Zyklen zu vermeiden. weak hat leichten Overhead (bei der Runtime für Nil-Setting registriert).

swift
class Person {
    var name: String
    var apartment: Apartment?  // Strong (person owns apartment)
    init(name: String) { self.name = name }
    deinit { print("\(name) deinit") }
}

class Apartment {
    var unit: String
    weak var tenant: Person?  // Weak (breaks cycle)
    init(unit: String) { self.unit = unit }
    deinit { print("Apartment \(unit) deinit") }
}

var alice: Person? = Person(name: "Alice")
var apt: Apartment? = Apartment(unit: "4A")

alice!.apartment = apt
apt!.tenant = alice  // Weak reference, no cycle

alice = nil  // Alice deinit (apartment's tenant becomes nil)
apt = nil    // Apartment deinit

// weak must be optional (becomes nil when deallocated)

Unowned-Referenzen

unowned-Referenzen halten, wie weak, das Objekt nicht am Leben. Im Gegensatz zu weak ist unowned nicht-optional und wird nicht nil. Der Zugriff auf eine unowned-Referenz nach Deallokation stürzt ab. Verwenden Sie unowned, wenn Sie garantieren können, dass das referenzierte Objekt die Referenz überlebt (z. B. eine Kreditkarte kann ohne ihren Kunden nicht existieren). unowned hat weniger Overhead als weak. Wahl: weak für unsichere Lebensdauern, unowned für garantierte Lebensdauern.

swift
class Customer {
    let name: String
    var card: CreditCard?  // Strong
    init(name: String) { self.name = name }
    deinit { print("\(name) deinit") }
}

class CreditCard {
    let number: UInt
    unowned let customer: Customer  // Non-optional, no cycle
    init(number: UInt, customer: Customer) {
        self.number = number
        self.customer = customer
    }
    deinit { print("Card \(number) deinit") }
}

var john: Customer? = Customer(name: "John")
john!.card = CreditCard(number: 1234, customer: john!)

john = nil
// Customer deinit, then CreditCard deinit

// unowned: non-optional, assumed to always have a value
// CRASH if accessed after deallocation
// Use when the referenced object has same or longer lifetime

Closures & Capture-Listen

Closures erfassen Referenzen standardmäßig stark und verursachen Zyklen, wenn die Closure auf self gespeichert wird. Capture-Listen ([weak self] oder [unowned self]) brechen den Zyklus. weak self erfordert optionale Behandlung (guard let self = self). Erfassen Sie spezifische Werte (let name = self.name), um self gar nicht erst zu erfassen. Closures, die an Funktionen weitergegeben werden (nicht gespeichert), verursachen keine Zyklen. Verwenden Sie immer Capture-Listen für gespeicherte Closures, die self referenzieren.

swift
class ViewController {
    var name = "MyVC"
    var onComplete: (() -> Void)?

    // BAD: Strong reference cycle
    func setupBad() {
        onComplete = {
            print("\(self.name) completed")  // Captures self strongly
        }
    }

    // GOOD: Capture list breaks cycle
    func setupGood() {
        onComplete = { [weak self] in
            guard let self = self else { return }
            print("\(self.name) completed")
        }
    }

    // unowned for guaranteed lifetime
    func setupUnowned() {
        onComplete = { [unowned self] in
            print("\(self.name) completed")
        }
    }

    // Capture specific values
    func setupCapture() {
        let name = self.name  // Capture value, not self
        onComplete = {
            print("\(name) completed")
        }
    }

    deinit { print("VC deinit") }
}

Leaks erkennen & beheben

Häufige Leak-Quellen: Delegates stark gespeichert (verwenden Sie weak), Timer hält Target (in deinit invalidieren), NotificationCenter-Beobachter (in deinit entfernen). Der Xcode Memory Graph Debugger visualisiert Objektgraphen und hebt Leaks hervor. Führen Sie mit MallocStackLogging aus, um Allocation-Traces zu erhalten. Testen Sie deinit, indem Sie Referenzen auf nil setzen und verifizieren, dass deinit läuft. Instruments (Leaks-Tool) erkennt Leaks zur Laufzeit. Paaren Sie immer Setup mit Teardown.

swift
// Xcode memory graph debugger: visualize reference cycles
// Product > Debug > View Memory Graph Hierarchy

// Common leak patterns:
// 1. Delegate stored as strong
class View {
    weak var delegate: ViewDelegate?  // MUST be weak
}

// 2. Timer retains target
class TimerHolder {
    var timer: Timer?
    func start() {
        // BAD: timer retains self via #selector
        timer = Timer.scheduledTimer(timeInterval: 1,
            target: self, selector: #selector(fire),
            userInfo: nil, repeats: true)
    }
    @objc func fire() {}
    deinit { timer?.invalidate() }  // MUST invalidate
}

// 3. NotificationCenter observer
class Observer {
    init() {
        NotificationCenter.default.addObserver(
            self, selector: #selector(handle),
            name: .someNotification, object: nil)
    }
    @objc func handle() {}
    deinit {
        NotificationCenter.default.removeObserver(self)
    }
}

// Use Xcode's Debug Memory Graph to find leaks
20

Actors & async/await

async/await-Grundlagen

async/await (Swift 5.5+) macht asynchronen Code synchron aussehend. async markiert Funktionen, die suspendieren können; await markiert Suspendierungspunkte. Tasks überbrücken Sync- zu Async-Kontexten. async let startet eine nebenläufige Task; await sammelt das Ergebnis. Verwenden Sie async let für parallele Ausführung, reguläres await für sequenziell. Fehler propagieren mit throws/try. Der Compiler erzwingt await an Suspendierungspunkten. async/await ersetzt Completion-Handler und Combine für viele Anwendungsfälle.

swift
// Async function
func fetchUser(id: Int) async throws -> User {
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode(User.self, from: data)
}

// Calling async functions
func loadProfile() async {
    do {
        let user = try await fetchUser(id: 1)
        print("Loaded: \(user.name)")
    } catch {
        print("Error: \(error)")
    }
}

// Task: bridge async to sync context
Task {
    await loadProfile()
}

// Concurrent execution
func loadMultiple() async {
    async let user1 = fetchUser(id: 1)
    async let user2 = fetchUser(id: 2)
    async let user3 = fetchUser(id: 3)

    let users = try await [user1, user2, user3]
    print("Loaded \(users.count) users")
}

// Sequenced vs concurrent
let sequential = try await fetchUser(id: 1)  // Waits
let concurrent = try await fetchUser(id: 2)  // Then waits again

Actors

Actors (Swift 5.5+) sind Referenztypen mit serialisiertem Zugriff und verhindern Data Races. Nur eine Methode führt gleichzeitig auf einem Actor aus – kein manuelles Locking nötig. Actor-Methoden müssen mit await aufgerufen werden (sie können suspendieren). Properties sind isoliert und können nicht direkt von außen zugegriffen werden. Actors sind der sichere Standard für geteilten veränderlichen Zustand in nebenläufigem Code. Verwenden Sie Actors statt Klassen mit Locks für meisten nebenläufigen Zustand.

swift
// Actor: reference type with serialized access
actor BankAccount {
    private var balance: Decimal

    init(balance: Decimal) {
        self.balance = balance
    }

    func deposit(_ amount: Decimal) {
        balance += amount
    }

    func withdraw(_ amount: Decimal) -> Bool {
        guard balance >= amount else { return false }
        balance -= amount
        return true
    }

    func getBalance() -> Decimal { balance }
}

// Usage (must be async)
let account = BankAccount(balance: 100)
Task {
    await account.deposit(50)
    let balance = await account.getBalance()
    print("Balance: \(balance)")  // 150
}

// Actors prevent data races automatically
// Only one method runs at a time per actor instance
// No need for locks or queues

Actor-Isolation

Actor-Isolation schützt Zustand. nonisolated-Member können ohne await zugegriffen werden (für reine Werte oder Methoden, die isolierten Zustand nicht berühren). @MainActor ist ein globaler Actor für Main-Thread-Isolation (UI-Updates). @globalActor erstellt benutzerdefinierte globale Actors. Das Überqueren von Actor-Grenzen erfordert await. Der Compiler verifiziert Isolation zur Kompilierzeit und verhindert Data Races. Verwenden Sie @MainActor für ViewModels und UI-Code; benutzerdefinierte Actors für domänenspezifische Isolation.

swift
actor Counter {
    private var count = 0

    // Isolated method (default)
    func increment() { count += 1 }

    // Nonisolated: can be called without await
    nonisolated var description: String { "Counter" }

    // nonisolated function
    nonisolated func id() -> String { "counter-\(ObjectIdentifier(self))" }
}

// Global actor: @MainActor runs on main thread
@MainActor
class ViewModel: ObservableObject {
    @Published var data: [Item] = []

    func loadData() async {
        // Always runs on main thread
        data = try await fetchItems()
    }
}

// Mark specific functions
class Service {
    @MainActor
    func updateUI(_ result: Result) {
        // UI updates on main thread
    }
}

// globalActor lets you apply to types, functions, properties
@globalActor
actor MyActor {
    static let shared = MyActor()
}

Strukturierte Nebenläufigkeit

Task-Gruppen ermöglichen dynamische parallele Berechnung. addTask spawnt Child-Tasks; for await group sammelt Ergebnisse, sobald sie fertig sind. Task-Gruppen erzwingen strukturierte Nebenläufigkeit: alle Children vervollständigen, bevor der Parent fortfährt. Abbruch propagiert von Parent zu Children automatisch. Prüfen Sie Task.isCancelled in langlaufenden Tasks. withThrowingTaskGroup propagiert Fehler und bricht Geschwister bei throw ab. Verwenden Sie Task-Gruppen für Fan-Out/Fan-In-Pattern.

swift
// TaskGroup: dynamic parallel computation
func fetchAllUsers(ids: [Int]) async throws -> [User] {
    try await withThrowingTaskGroup(of: User.self) { group in
        for id in ids {
            group.addTask { try await fetchUser(id: id) }
        }

        var users: [User] = []
        for try await user in group {
            users.append(user)
        }
        return users
    }
}

// Task tree: parent-child relationship
func process() async {
    await withTaskGroup(of: Void.self) { group in
        for i in 1...5 {
            group.addTask {
                await processItem(i)
            }
        }
        // All tasks complete before group returns
    }
    print("All done")
}

// Cancellation
func longRunning() async {
    await withTaskGroup(of: Void.self) { group in
        group.addTask {
            for i in 1...100 {
                if Task.isCancelled { return }
                await doWork(i)
            }
        }
    }
}

// Cancel from outside
let task = Task { await longRunning() }
task.cancel()

AsyncSequence & Streams

AsyncSequence ist die Async-Version von Sequence und ermöglicht for await-Schleifen. AsyncStream überbrückt Callback-basierte oder Delegate-basierte APIs zu async/await. yield emitiert Werte, finish terminiert. onTermination räumt Ressourcen auf. AsyncThrowingStream unterstützt Fehler. Verwenden Sie AsyncSequence für pagenierte APIs, Echtzeitdaten oder jeden Stream von Werten über Zeit. Die Standardbibliothek bietet .lines auf URL für zeilenweises Dateilesen. AsyncSequence integriert sich mit Task-Abbruch.

swift
// AsyncSequence: iterate asynchronously
struct Counter: AsyncSequence {
    struct AsyncIterator: AsyncIteratorProtocol {
        var current: Int
        let max: Int
        mutating func next() async -> Int? {
            guard current <= max else { return nil }
            defer { current += 1 }
            try? await Task.sleep(nanoseconds: 100_000_000)
            return current
        }
    }

    let start: Int
    let max: Int

    func makeAsyncIterator() -> AsyncIterator {
        AsyncIterator(current: start, max: max)
    }
}

// Usage
for await num in Counter(start: 1, max: 5) {
    print(num)  // 1 2 3 4 5 (with delays)
}

// AsyncStream: bridge callbacks to async
func urlLines(_ url: URL) -> AsyncThrowingStream<String, Error> {
    AsyncThrowingStream { continuation in
        let task = Task {
            do {
                for try await line in url.lines {
                    continuation.yield(line)
                }
                continuation.finish()
            } catch {
                continuation.finish(throwing: error)
            }
        }
        continuation.onTermination = { _ in task.cancel() }
    }
}
21

Testing

XCTest-Grundlagen

XCTest ist das Standard-Test-Framework. XCTestCase gruppiert verwandte Tests. setUp/tearDown laufen vor/nach jedem Test. Assertionen: XCTAssertEqual, XCTAssertNil, XCTAssertThrowsError, XCTAssertTrue. @testable import greift auf interne Symbole zu. Tests laufen standardmäßig auf dem Main-Thread. Verwenden Sie XCTAssertNotNil für Optionals. Führen Sie Tests mit Cmd+U aus. Xcode zeigt Testergebnisse inline. Schreiben Sie Tests neben Code für schnelles Feedback.

swift
import XCTest
@testable import MyApp

class UserTests: XCTestCase {
    var user: User!

    override func setUp() {
        super.setUp()
        user = User(name: "Alice", age: 30)
    }

    override func tearDown() {
        user = nil
        super.tearDown()
    }

    func testName() {
        XCTAssertEqual(user.name, "Alice")
    }

    func testAgeAfterBirthday() {
        user.haveBirthday()
        XCTAssertEqual(user.age, 31)
    }

    func testValidation() {
        XCTAssertThrowsError(try User(name: "", age: -1)) { error in
            guard let error = error as? UserError else {
                XCTFail("Wrong error type")
                return
            }
            XCTAssertEqual(error, .invalidName)
        }
    }

    func testOptional() {
        XCTAssertNotNil(user.email)
        XCTAssertNil(user.address)
    }
}

// Run: Cmd+U in Xcode

Async-Testing

Swift 5.5+ unterstützt Async-Testmethoden direkt (async throws func test...). Verwenden Sie try await innerhalb von Tests. Für älteren Code verwenden Sie XCTestExpectation: Expectation erstellen, fulfill() im Callback, wait(for:timeout:). Setzen Sie vernünftige Timeouts, um Hänger zu erkennen. Für das Testen von Async-Sequenzen verwenden Sie for await-Schleifen. Mocken Sie Netzwerkaufrufe mit URLProtocol oder Dependency Injection. Testen Sie sowohl Erfolgs- als auch Fehlerpfade. Async-Tests laufen standardmäßig nebenläufig – verwenden Sie .serialized-Trait, wenn Reihenfolge wichtig ist.

swift
class APITests: XCTestCase {
    func testFetchUser() async throws {
        let user = try await fetchUser(id: 1)
        XCTAssertEqual(user.name, "Alice")
    }

    func testFetchFailure() async {
        do {
            _ = try await fetchUser(id: -1)
            XCTFail("Should have thrown")
        } catch {
            // Expected
        }
    }

    // Old style with expectations (still works)
    func testAsyncWithExpectation() {
        let expectation = XCTestExpectation(description: "Fetch completes")

        fetchUserAsync { result in
            switch result {
            case .success(let user):
                XCTAssertEqual(user.name, "Alice")
            case .failure:
                XCTFail("Should succeed")
            }
            expectation.fulfill()
        }

        wait(for: [expectation], timeout: 5)
    }
}

Mocking & Stubbing

Dependency Injection ermöglicht Testen: Definieren Sie Protocols für Abhängigkeiten, injizieren Sie Mocks in Tests. MockNetworkService gibt vorbestimmte Ergebnisse zurück. Testen Sie Erfolgs- und Fehlerfälle durch Setzen von mock.fetchUserResult. Dieses Pattern entkoppelt Tests vom Netzwerk und macht sie schnell und deterministisch. Für komplexe Mocks verwenden Sie Bibliotheken wie Cuckoo oder Mockingbird. Testen Sie die öffentliche API Ihrer Typen, nicht Implementierungsdetails. Mocks sollten einfach und fokussiert sein.

swift
// Protocol for dependency injection
protocol NetworkService {
    func fetchUser(id: Int) async throws -> User
}

// Production implementation
class APIService: NetworkService {
    func fetchUser(id: Int) async throws -> User {
        // Real network call
    }
}

// Mock for testing
class MockNetworkService: NetworkService {
    var fetchUserResult: Result<User, Error>?

    func fetchUser(id: Int) async throws -> User {
        switch fetchUserResult {
        case .success(let user): return user
        case .failure(let error): throw error
        case .none: throw URLError(.badURL)
        }
    }
}

class ViewModelTests: XCTestCase {
    func testLoadUser() async throws {
        let mock = MockNetworkService()
        mock.fetchUserResult = .success(User(name: "Alice", age: 30))

        let vm = ViewModel(service: mock)
        await vm.load()

        XCTAssertEqual(vm.userName, "Alice")
    }
}

UI-Testing

UI-Tests automatisieren Benutzerinteraktionen über Accessibility-Identifikatoren. XCUIApplication startet die App. Finden Sie Elemente über Accessibility-Identifikator (im Code mit .accessibilityIdentifier setzen). Aktionen: tap, typeText, swipeLeft/Right. Assertionen: waitForExistence, exists, hittable. UI-Tests sind langsamer als Unit-Tests, finden aber Integrations-Bugs. Setzen Sie Accessibility-Identifikatoren in SwiftUI mit .accessibilityIdentifier("Email"). Führen Sie UI-Tests auf mehreren Geräten/Simulatoren für Abdeckung aus.

swift
import XCTest

class AppUITests: XCTestCase {
    override func setUpWithError() throws {
        continueAfterFailure = false
        let app = XCUIApplication()
        app.launch()
    }

    func testLoginFlow() {
        let app = XCUIApplication()

        let emailField = app.textFields["Email"]
        let passwordField = app.secureTextFields["Password"]
        let loginButton = app.buttons["Log In"]

        emailField.tap()
        emailField.typeText("[email protected]")

        passwordField.tap()
        passwordField.typeText("password123")

        loginButton.tap()

        // Verify navigation
        XCTAssertTrue(app.staticTexts["Welcome, Alice!"].waitForExistence(timeout: 5))
    }

    func testSwipeAndTap() {
        let cell = app.cells["Item 5"]
        cell.swipeLeft()
        app.buttons["Delete"].tap()
        XCTAssertFalse(cell.exists)
    }
}

Performance & Code-Abdeckung

measure benchmarkt Code-Performance und vergleicht mit Baselines. Setzen Sie Baselines in Xcode; Tests fehlschlagen, wenn Regression den Schwellwert überschreitet. measureMetrics verfolgt spezifische Metriken (Peak-Memory, CPU). Aktivieren Sie Code-Abdeckung im Scheme, um ungetestete Codepfade zu sehen. Streben Sie 80%+ Abdeckung für Geschäftslogik an. Snapshot-Testing (swift-snapshot-testing) erfasst UI für visuelle Regression. Profilieren Sie mit Instruments (Time Profiler, Allocations) für tiefere Analyse. Performance-Tests erkennen Regressionen früh.

swift
class PerformanceTests: XCTestCase {
    func testSortingPerformance() {
        let data = (1...10000).shuffled()

        measure {
            _ = data.sorted()
        }
    }

    func testMemoryUsage() {
        measureMetrics([XCTPerformanceMetric.peakMemory]) {
            _ = (0..<10000).map { _ in [Int](repeating: 0, count: 100) }
        }
    }

    // Baseline comparison
    func testWithBaselines() {
        let options = XCTMeasureOptions()
        options.defaultOptions.measurementUnits = .seconds

        measure(options) {
            // Code to benchmark
        }
    }
}

// Code coverage: enable in Edit Scheme > Test > Options
// View coverage: Cmd+Shift+Y (Report Navigator) > Coverage

// Snapshot testing (third-party)
// func testAppearance() {
//     assertSnapshot(of: view, as: .image)
// }
22

Swift-Nebenläufigkeit

async/await

async/await (Swift 5.5+) vereinfacht Async-Code. async markiert Funktionen, die suspendieren können. await markiert Suspendierungspunkte. Task erstellt einen Async-Kontext. Viel sauberer als Completion-Handler. Fehler propagieren mit try.

swift
func fetchData() async throws -> String {
    let (data, _) = try await URLSession.shared.data(from: url)
    return String(data: data, encoding: .utf8) ?? ""
}
// Call from async context
Task {
    let result = try await fetchData()
    print(result)
}

Actors

actor (Swift 5.5+) ist ein Referenztyp mit automatischem Mutual Exclusion. Nur eine Task greift gleichzeitig auf seinen Zustand zu. Methoden sind implizit async, wenn von außen aufgerufen. Ersetzt Locks und Dispatch-Queues für geteilten veränderlichen Zustand. Thread-sicher per Design.

swift
actor Counter {
    private var count = 0
    func increment() {
        count += 1
    }
    func getValue() -> Int {
        count
    }
}
// Usage
Task {
    let counter = Counter()
    await counter.increment()
    let value = await counter.getValue()
}

Async Sequence

AsyncSequence ist das Async-Äquivalent von Sequence. for await iteriert asynchron. Nützlich für Streaming-Daten (Netzwerk, Dateizeilen). Implementieren Sie AsyncIteratorProtocol, um benutzerdefinierte Async-Sequenzen zu erstellen. Der Compiler behandelt Suspendierung und Abbruch.

swift
for try await item in asyncSequence {
    print(item)
}
// Custom AsyncSequence
struct Counter: AsyncSequence {
    struct AsyncIterator: AsyncIteratorProtocol {
        var current = 0
        mutating func next() async -> Int? {
            current += 1
            return current <= 5 ? current : nil
        }
    }
    func makeAsyncIterator() -> AsyncIterator { AsyncIterator() }
}

Task-Gruppen

Task-Gruppen führen mehrere Tasks nebenläufig aus und sammeln Ergebnisse. addTask fügt eine Child-Task hinzu. for await iteriert Ergebnisse, sobald sie fertig sind. Alle Tasks müssen vervollständigen, bevor die Gruppe zurückkehrt. Strukturierte Nebenläufigkeit: Abbruch propagiert zu Children. Ergebnisse werden sicher gesammelt.

swift
let results = await withTaskGroup(of: Int.self) { group in
    for i in 1...5 {
        group.addTask { i * i }
    }
    var sum = 0
    for await result in group {
        sum += result
    }
    return sum
}

Continuation

withCheckedContinuation überbrückt Completion-Handler-APIs zu async/await. resume(returning:) setzt die Async-Funktion fort. Muss genau einmal aufgerufen werden. withCheckedThrowingContinuation unterstützt Fehler. Nützlich für die Adoption von async/await mit vorhandenen APIs.

swift
func fetchWithCompletion(_ completion: @escaping (String) -> Void) {
    // Legacy completion handler API
    completion("data")
}
// Wrap in async
func fetchAsync() async -> String {
    await withCheckedContinuation { continuation in
        fetchWithCompletion { result in
            continuation.resume(returning: result)
        }
    }
}
23

SwiftUI Deep

View-Modifier

Modifier wrappen Views, um Erscheinung oder Verhalten zu ändern. Reihenfolge ist wichtig: spätere Modifier wrappen frühere. .padding vor .background setzt Padding in den Hintergrund. Modifier geben neue View-Instanzen zurück. Verketten Sie sie für komplexes Styling.

swift
Text("Hello")
    .font(.title)
    .foregroundColor(.blue)
    .padding()
    .background(Color.gray)
    .cornerRadius(8)
    .shadow(radius: 4)

Listen

List zeigt scrollbare Zeilen. ForEach generiert Zeilen aus Daten. onDelete ermöglicht Wischen-zum-Löschen. Andere Modifier: onMove, onInsert. Identifiable-Daten (id-Property) sind für ForEach erforderlich. List-Style: .plain, .insetGrouped, .sidebar.

swift
List {
    ForEach(items) { item in
        HStack {
            Text(item.name)
            Spacer()
            Text("\(item.price)")
        }
    }
    .onDelete { indexSet in
        items.remove(atOffsets: indexSet)
    }
}
.listStyle(.insetGrouped)

Navigation

NavigationStack verwaltet einen Stack von Views. NavigationLink pusht ein Ziel. navigationTitle setzt den Titel. toolbar fügt Navigationsleisten-Buttons hinzu. Für iOS 16+. Verwenden Sie NavigationView für ältere Versionen. Sheets und Alerts verwenden .sheet- und .alert-Modifier.

swift
NavigationStack {
    List(items) { item in
        NavigationLink(item.name) {
            DetailView(item: item)
        }
    }
    .navigationTitle("Items")
    .toolbar {
        ToolbarItem(placement: .navigationBarTrailing) {
            Button("Add") { addItem() }
        }
    }
}

State-Management

@State für lokalen View-Zustand (Werttypen). @Binding für weitergegebene State. @StateObject für eigene Observable Objects (einmal erstellt). @ObservedObject für externe Observable Objects. @EnvironmentObject für App-weite State. @State-Änderungen lösen View-Updates aus.

swift
struct CounterView: View {
    @State private var count = 0
    @Binding var selected: Int
    @StateObject var viewModel = ViewModel()
    var body: some View {
        Button("\(count)") { count += 1 }
    }
}

Animationen

withAnimation animiert State-Änderungen. .spring, .easeInOut sind Animationskurven. .animation(value:) animiert, wenn sich der Wert ändert. .scaleEffect, .opacity, .offset sind animierbar. Implizite Animationen verwenden .animation-Modifier. Explizit verwenden withAnimation.

swift
struct AnimatedView: View {
    @State private var scale: CGFloat = 1.0
    var body: some View {
        Button("Tap") {
            withAnimation(.spring(response: 0.3)) {
                scale = scale == 1 ? 1.5 : 1
            }
        }
        .scaleEffect(scale)
        .animation(.easeInOut(duration: 0.3), value: scale)
    }
}
24

Combine Framework

Publisher & Subscriber

Combine ist Apples reaktives Framework. Publisher emitieren Werte über Zeit. Subscriber empfangen sie. sink erstellt einen Subscriber. Just emitiert einen einzelnen Wert. PassthroughSubject ist ein manueller Publisher. Cancellables müssen gehalten werden oder das Abonnement bricht ab. Ähnlich wie RxSwift.

swift
import Combine

let publisher = Just(42)
let cancellable = publisher.sink { value in
    print("Received: \(value)")
}
// Just emits one value then finishes
// PassthroughSubject: manual values
let subject = PassthroughSubject<Int, Never>()
subject.send(1)
subject.send(2)

Operatoren

Operatoren transformieren Publisher-Output. map transformiert Werte. filter wählt Werte aus. debounce verzögert bis Ruhe. throttle limitiert Rate. combineLatest merged Streams. flatMap verkettet Publisher. Operatoren sind lazy: nichts passiert, bis sink abonniert.

swift
let publisher = (1...10).publisher
    .map { $0 * 2 }
    .filter { $0 > 10 }
    .sink { print($0) }  // 12, 14, 16, 18, 20

// Debounce
let debounced = subject
    .debounce(for: .seconds(0.5), scheduler: RunLoop.main)
    .sink { print($0) }

@Published

@Published exponiert eine Property als Combine-Publisher. $-Präfix greift auf den Publisher zu. Änderungen emitieren neue Werte. Funktioniert mit ObservableObject für SwiftUI. Der Publisher emitiert den aktuellen Wert bei Abonnement. Nützlich für reaktive UI-Updates ohne manuelle Notifications.

swift
class ViewModel: ObservableObject {
    @Published var count = 0
}
let vm = ViewModel()
let cancellable = vm.$count.sink { newCount in
    print("Count changed: \(newCount)")
}
vm.count = 1  // Prints "Count changed: 1"
vm.count = 2  // Prints "Count changed: 2"

Future

Future wickelt eine Completion-Handler-API als Publisher. promise(.success) emitiert einen Wert. promise(.failure) emitiert einen Fehler. Future emitiert genau einmal. Nützlich zum Überbrücken von Callback-APIs zu Combine. Die Closure läuft eager, außer mit .delay oder deferred gewrapped.

swift
func fetchUser() -> Future<User, Error> {
    Future { promise in
        URLSession.shared.dataTask(with: url) { data, _, error in
            if let error = error { promise(.failure(error)) }
            else { promise(.success(parse(data!))) }
        }.resume()
    }
}
// Usage
fetchUser().sink(receiveCompletion: { _ in }, receiveValue: { user in print(user) })

Fehlerbehandlung

Fail emitiert sofort einen Fehler. catch ersetzt einen fehlgeschlagenen Publisher durch einen anderen. retry abonniert bei Fehlschlag erneut. assertNoFailure stürzt bei Fehler ab (für Debugging). Das Completion-Event signalisiert keine weiteren Werte. Fehler propagieren downstream, außer gefangen.

swift
let publisher = Fail<Int, MyError>(error: .notFound)
    .catch { error in
        Just(0)  // Fallback value
    }
    .sink { print($0) }  // 0

// retry on failure
let retried = requestPublisher
    .retry(3)
    .sink(receiveCompletion: { _ in }, receiveValue: { _ in })
25

Speicherverwaltung

ARC-Grundlagen

ARC (Automatic Reference Counting) verfolgt Referenzen. Wenn der Zähler null erreicht, läuft deinit und Speicher wird freigegeben. Starke Referenzen inkrementieren den Zähler. Referenzzyklen verhindern Deallokation. ARC ist deterministisch (im Gegensatz zu GC). deinit läuft synchron, wenn die letzte Referenz fällt.

swift
class Person {
    let name: String
    init(name: String) { self.name = name }
    deinit { print("\(name) is being deinitialized") }
}
var p1: Person? = Person(name: "Alice")
p1 = nil  // Prints "Alice is being deinitialized"

Schwache Referenzen

weak-Referenzen inkrementieren nicht den Retain-Count. Sie auto-setzen auf nil, wenn das Objekt deallokiert. Muss optional var sein. Verwenden Sie für Delegates und Beobachter, um Zyklen zu brechen. weak-Referenzen sind zeroing: sicherer Zugriff, nachdem das Objekt weg ist. Die häufigste Fix für Retain-Zyklen.

swift
class View {
    weak var delegate: Delegate?
}
class Delegate {
    var view: View?  // strong
}
// weak: does not keep object alive, auto-set to nil
// Use for delegate patterns to avoid cycles

Unowned-Referenzen

unowned-Referenzen inkrementieren nicht den Retain-Count. Im Gegensatz zu weak sind sie nicht-optional und nicht zeroing. Zugriff nach Dealloc stürzt ab. Verwenden Sie, wenn das referenzierte Objekt den Referenzer überlebt oder mit ihm stirbt. Schneller als weak (kein nil-Check). Häufig in Closures, die self erfassen, wenn self die Closure überlebt.

swift
class Customer {
    var card: CreditCard?
}
class CreditCard {
    unowned let customer: Customer
    init(customer: Customer) { self.customer = customer }
}
// unowned: does not keep alive, but not optional
// CRASHES if accessed after dealloc
// Use when the other object has same or shorter lifetime

Retain-Zyklen

Retain-Zyklen entstehen, wenn Objekte sich gegenseitig stark referenzieren. Keiner deallokiert, Speicher leakt. Fix mit weak oder unowned. Häufig in Delegates, Beobachtern und Closures. Closures erfassen self standardmäßig stark. [weak self] oder [unowned self] bricht den Zyklus. Verwenden Sie Memory Graph Debugger, um Zyklen zu finden.

swift
// BAD: retain cycle
class Node {
    var next: Node?
    var prev: Node?  // strong -> cycle
}
// GOOD: break with weak
class Node {
    var next: Node?
    weak var prev: Node?
}
// Closures: use [weak self]
{ [weak self] in self?.update() }

Closures & Capture

Closures erfassen Variablen per Referenz. Gespeicherte Closures (escaping) erstellen Retain-Zyklen, wenn sie self erfassen. [weak self] macht die Erfassung optional und weak. @escaping markiert Closures, die den Funktionsaufruf überleben. Non-escaping Closures (Standard) können keine Zyklen verursachen. Der Compiler warnt vor potenziellen Zyklen.

swift
class Timer {
    var handler: (() -> Void)?
    func start() {
        handler = { [weak self] in
            self?.tick()  // weak to avoid cycle
        }
    }
    func tick() { /* ... */ }
}
// Escaping closure: stored for later
// @escaping marks escaping closures
26

Fehlerbehandlung Deep

Benutzerdefinierte Fehler

Benutzerdefinierte Fehler konformieren zu Error. LocalizedError bietet errorDescription. Assoziierte Werte tragen Kontext. Enums sind der idiomatische Weg, Fehler zu definieren. Jeder Case repräsentiert einen unterschiedlichen Fehlschlag. Switch-Erschöpftheit stellt sicher, dass alle Fehler behandelt werden. Konformieren Sie zu CustomStringConvertible für Debug-Output.

swift
enum APIError: Error, LocalizedError {
    case invalidURL
    case unauthorized
    case serverError(Int)
    case decodingFailed(Error)

    var errorDescription: String? {
        switch self {
        case .invalidURL: return "Invalid URL"
        case .unauthorized: return "Unauthorized"
        case .serverError(let code): return "Server error: \(code)"
        case .decodingFailed(let err): return "Decode failed: \(err)"
        }
    }
}

Result-Typ

Result ist ein typisiertes Erfolg/Fehlschlag-Enum. Nützlich für synchrone Operationen, die fehlschlagen können. map transformiert den Erfolgswert. flatMap verkettet Operationen. get() wirft, um zu try/catch zu konvertieren. Result wird werfenden Funktionen für gespeicherte oder weitergegebene Ergebnisse vorgezogen. Kombiniert Fehlerbehandlung mit Wertsemantik.

swift
func fetchUser(id: Int) -> Result<User, APIError> {
    guard id > 0 else { return .failure(.invalidURL) }
    return .success(User(name: "Alice"))
}
// Usage
switch fetchUser(id: 1) {
case .success(let user): print(user)
case .failure(let error): print(error)
}
// map and flatMap
let result = fetchUser(id: 1).map { $0.name }

Rethrows

rethrows propagiert Fehler von einem Closure-Parameter. Wenn die Closure nicht wirft, wirft die Funktion auch nicht. Dies vermeidet das Erzwingen von try bei Aufrufern, die nicht-werfende Closures weitergeben. Verwendet von map, filter und anderen Higher-Order Functions. Die Funktion selbst kann nicht unabhängig von der Closure werfen.

swift
func withLock<T>(_ lock: NSLock, _ body: () throws -> T) rethrows -> T {
    lock.lock()
    defer { lock.unlock() }
    return try body()
}
// Only throws if body throws
let x = withLock(lock) { 5 }  // no try needed
let y = try withLock(lock) { try throwingFunc() }

Fehler-Propagation

try propagiert Fehler an den Aufrufer. try? konvertiert Fehler zu nil (gibt Optional zurück). try! stürzt bei Fehler ab (verwenden Sie, wenn sicher). Fehler blubbern den Call-Stack hinauf, bis gefangen. Funktionen, die werfen, müssen mit throws markiert werden. Der Compiler erzwingt try-Marker und verhindert unbehandelte Fehler. defer läuft unabhängig von geworfenen Fehlern.

swift
func process() throws {
    let data = try fetchData()  // may throw
    let user = try parse(data)  // may throw
    save(user)                  // does not throw
}
// try?: converts to optional, nil on error
let user = try? fetchUser()
// try!: asserts no error, crashes if thrown
let user = try! fetchUser()

Cleanup mit defer

defer plant Cleanup zur Ausführung, wenn der Scope verlassen wird. Läuft unabhängig davon, wie der Scope verlassen wird (return, throw oder Fall-Through). Mehrere defers laufen in LIFO-Reihenfolge. Nützlich für Schließen von Dateien, Freigeben von Locks, Befreien von Ressourcen. defer kann nicht throw oder break/continue. Die Closure erfasst Variablen per Referenz.

swift
func processFile() throws {
    let file = open("file.txt")
    defer { close(file) }  // runs on scope exit

    let data = try read(file)
    // ... process ...
    // close() runs here even if read throws
}
// Multiple defers run in reverse order (LIFO)
defer { print("1") }
defer { print("2") }  // prints 2 then 1

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.