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.
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)) // IntOptionals
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).
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.
let person = (name: "Alice", age: 30)
print(person.name) // Alice
print(person.0) // Alice
let (n, a) = person
print("\(n), \(a)") // Alice, 30Typumwandlung
Swift konvertiert nicht implizit zwischen Typen. Verwenden Sie immer explizite Konvertierung (z. B. Double(intVal)), um Mehrdeutigkeit zu vermeiden und subtile Bugs zu verhindern.
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.
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 tooStrings & 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.
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 = 8Hä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.
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.
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.
let poem = """
Roses are red,
Violets are blue,
Swift is great,
And so are you.
"""
print(poem)
// Use \\(expr) for interpolation in multilineString-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.
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())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.
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.
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.
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.
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.
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)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.
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.
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.
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.
var n = 5
while n > 0 {
print(n)
n -= 1
}
var x = 0
repeat {
x += 1
} while x < 3
print(x) // 3Guard (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.
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)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+).
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)) // 7Mehrere 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.
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.
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).
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.
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 10Klassen & 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.
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.0Class (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.
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) // AliceVererbung & 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.
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.
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.
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 10Protocols & 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#.
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, AliceProtocol-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.
protocol Describable {
func describe() -> String
}
extension Describable {
func describe() -> String { "A \(type(of: self))" }
}
struct Box: Describable { }
let b = Box()
print(b.describe()) // A BoxExtensions
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.
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 timesGenerics
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.
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.
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] }
}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.
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.
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.
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.
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.
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)")
}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.
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.
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) // deleteDate & 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.
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.
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) // AliceDatumsberechnungen
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.
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!) // 24Nebenlä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.
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.
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 completionActors (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.
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 racesGCD (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.
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.
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 { ... }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.
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.
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 // 6Protocol-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.
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.
// 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).
// 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 */ }
}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.
// 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.
// 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.
// 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.
// 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.
// 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()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.
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.
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).
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.
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.
// 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 exclusivitySwiftUI-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.
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.
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.
// 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.
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.
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")
}
}
}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.
// 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.
// 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 200Escaping 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.
// @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.
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 = 56Closures 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.
// 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)
}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.
@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) // 0Projected 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.
@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.
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.
@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.
// 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.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.
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.
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.
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.
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.
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)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.
// 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 typeExtensions 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.
// 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.
// 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 boundariesInitialisierer 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.
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.
// 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 { /* ... */ }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.
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 deinitializedSchwache 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).
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.
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 lifetimeClosures & 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.
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.
// 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 leaksActors & 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.
// 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 againActors
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.
// 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 queuesActor-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.
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.
// 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.
// 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() }
}
}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.
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 XcodeAsync-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.
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.
// 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.
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.
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)
// }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.
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.
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.
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.
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.
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)
}
}
}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.
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.
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.
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.
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.
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)
}
}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.
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.
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.
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.
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.
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 })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.
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.
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 cyclesUnowned-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.
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 lifetimeRetain-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.
// 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.
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 closuresFehlerbehandlung 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.
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.
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.
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.
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.
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 1Verwandte Swift-Snippets
Copy-paste ready code for common tasks.
Optionals
Use optional binding, nil-coalescing, chaining, and guard for safe unwrapping.
Closures
Define closure expressions, capture state, and pass escaping callbacks.
Protocols
Define contracts, conform with structs, and add default behavior via extensions.
Generics
Write type-parameterized functions and types with protocol constraints.
Structs and Classes
Compare value-type structs with reference-type classes and inheritance.
Error Handling
Throw and catch typed errors with do-catch, try?, try!, and rethrows.
Concurrency (async/await)
Run async functions, parallelize with async let, and fan out with task groups.
String Manipulation
Trim, split, join, replace, and index strings using Swift's Unicode API.
Was this helpful?