Einführung in die asynchrone Programmierung in Swift
\n\nModerne mobile Anwendungen kommen selten ohne Netzwerkanfragen, Datenbankarbeit oder die Verarbeitung großer Dateien aus. All diese Operationen benötigen Zeit, und wenn sie im Hauptthread ausgeführt werden, friert die Benutzeroberfläche der Anwendung ein und der Benutzer erhält eine unangenehme Erfahrung. Genau hier kommt die asynchrone Programmierung zur Hilfe.
\n\nVor der Einführung von Swift 5.5 verwendeten Entwickler Closures (Completion Handler) oder Drittanbieter-Bibliotheken (Combine, RxSwift), was oft zur „Callback-Hölle“ und schwer lesbarem Code führte. Mit der Einführung des neuen Swift Concurrency-Modells (async/await) ist das Schreiben von asynchronem Code genauso einfach und linear geworden wie synchroner Code.
\n\nIn diesem Leitfaden werden wir die wichtigsten Konzepte der asynchronen Programmierung in Swift anhand praktischer Beispiele erläutern. Sie erfahren, wie Sie async/await, Task, Actor und andere Werkzeuge verwenden, um schnelle und reaktionsfähige Anwendungen für iOS und macOS zu erstellen.
Grundlegende Konzepte von Swift Concurrency
\n\n1. Async und Await: Die Grundlage der Asynchronität
\n\nDie Schlüsselwörter async und await sind das Herzstück des neuen Modells. async deklariert eine Funktion als asynchron, und await pausiert die Ausführung, bis ein Ergebnis vorliegt, ohne dabei den Thread zu blockieren.
Betrachten wir ein einfaches Beispiel: das Laden von Daten aus dem Netzwerk.
\n\nimport Foundation\n\n// Deklarieren einer asynchronen Funktion\nfunc fetchUserData() async throws -> String {\n let url = URL(string: "https://api.example.com/user/1")!\n let (data, _) = try await URLSession.shared.data(from: url)\n return String(data: data, encoding: .utf8) ?? "Fehler"\n}\n\n// Aufruf der asynchronen Funktion\nTask {\n do {\n let userData = try await fetchUserData()\n print("Benutzerdaten: \\(userData)")\n } catch {\n print("Ladefehler: \\(error.localizedDescription)")\n }\n}\n\nBeachten Sie: try await bedeutet, dass wir auf ein Ergebnis warten, das einen Fehler auslösen kann. Der Code sieht aus wie normaler synchroner Code, blockiert aber nicht den Hauptthread.
2. Task: Einstiegspunkt in die asynchrone Welt
\n\nAsynchrone Funktionen können nur aus einem anderen asynchronen Kontext heraus aufgerufen werden. Um eine asynchrone Operation aus synchronem Code (z. B. aus viewDidLoad()) zu starten, wird Task verwendet.
class ViewController: UIViewController {\n \n override func viewDidLoad() {\n super.viewDidLoad()\n \n // Starten einer asynchronen Aufgabe\n Task {\n let data = await loadData()\n updateUI(with: data)\n }\n }\n \n func loadData() async -> String {\n // Simulation eines langen Ladevorgangs\n try? await Task.sleep(nanoseconds: 2_000_000_000)\n return "Geladene Daten"\n }\n \n func updateUI(with data: String) {\n print("UI aktualisieren: \\(data)")\n }\n}\n\nWichtig: Task erstellt eine neue asynchrone Aufgabe, die im Hintergrundthread ausgeführt wird. Für UI-Updates müssen Sie unbedingt zum Hauptthread zurückkehren – standardmäßig erledigt await dies automatisch.
3. Actor: Schutz vor Datenrennen
\n\nBei der Arbeit mit Multithreading tritt häufig das Problem des Datenrennens (Data Race) auf, wenn mehrere Threads gleichzeitig eine Variable ändern. Swift bietet eine elegante Lösung – Actor.
Actor ist ein Referenztyp, der garantiert, dass zu jedem Zeitpunkt nur ein Thread auf seinen veränderlichen Zustand zugreifen kann.
actor Counter {\n private var value = 0\n \n func increment() {\n value += 1\n }\n \n func getValue() -> Int {\n return value\n }\n}\n\n// Verwendung\nlet counter = Counter