Combine Framework: Reaktive Programmierung in Swift
Combine meistern, um asynchrone Datenströme in Swift zu beherrschen: Publishers, Subscribers, Operators und fortgeschrittene Muster für iOS-Apps.

Reaktive Programmierung verändert grundlegend, wie asynchrone Ereignisse und Datenströme in iOS-Anwendungen behandelt werden. Combine, Apples natives Framework, bietet einen deklarativen und typsicheren Ansatz, um komplexe Daten-Pipelines zu orchestrieren. Dieser Leitfaden führt von den Grundkonzepten bis hin zu produktionsreifen Mustern.
Combine ist ab iOS 13 fest integriert, liefert dank Apples Optimierungen bessere Leistung und fügt sich nahtlos in SwiftUI ein. Keine externen Abhängigkeiten zu pflegen.
Combine Kernkonzepte
Combine baut auf drei zentralen Konzepten auf: Publishers, die Werte ausgeben, Subscribers, die diese empfangen, und Operators, die Daten dazwischen transformieren. Diese Architektur erlaubt es, reaktive und kombinierbare Daten-Pipelines aufzubauen.
Publisher: die Datenquelle
Ein Publisher ist ein Typ, der eine Folge von Werten über die Zeit emittieren kann. Jeder Publisher deklariert zwei zugehörige Typen: den Typ des emittierten Werts (Output) und den Typ möglicher Fehler (Failure). So lassen sich verschiedene Publisher-Typen erstellen:
import Combine
// Just: emits a single value then completes
// Useful for converting a simple value to a Publisher
let singleValue = Just("Hello Combine")
// CurrentValueSubject: stores and emits the current value
// Perfect for representing state that changes over time
let counter = CurrentValueSubject<Int, Never>(0)
// PassthroughSubject: emits values without storing them
// Ideal for one-time events (taps, notifications)
let buttonTaps = PassthroughSubject<Void, Never>()
// Future: emits a single value asynchronously
// Wraps an async operation that returns a result
let asyncOperation = Future<String, Error> { promise in
// Simulate a network call
DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
promise(.success("Data loaded"))
}
}Der Typ Never für Fehler bedeutet, dass der Publisher niemals scheitern kann. Diese Garantie zur Compile-Zeit vereinfacht den Code zur Fehlerbehandlung.
Subscriber: Werte empfangen
Ein Subscriber abonniert einen Publisher, um dessen Werte zu erhalten. Die Methode sink ist die häufigste Art, einen Subscriber zu erstellen. Sie erwartet zwei Closures: eine für Fehler oder Abschluss und eine für jeden empfangenen Wert:
import Combine
// Variable to store subscriptions
// Without this reference, the subscription would be immediately cancelled
var cancellables = Set<AnyCancellable>()
let publisher = ["Swift", "Combine", "iOS"].publisher
// sink() creates a Subscriber that receives values
publisher
.sink(
// Called when the Publisher completes or fails
receiveCompletion: { completion in
switch completion {
case .finished:
print("✅ Completed successfully")
case .failure(let error):
print("❌ Error: \(error)")
}
},
// Called for each emitted value
receiveValue: { value in
print("Received: \(value)")
}
)
// store() keeps a reference to the subscription
.store(in: &cancellables)
// Output:
// Received: Swift
// Received: Combine
// Received: iOS
// ✅ Completed successfullyDas von sink() zurückgegebene AnyCancellable muss stets gespeichert werden. Ohne Referenz wird das Abonnement automatisch beendet und kein Wert empfangen.
Daten transformieren mit Operators
Operators sind das Herzstück von Combine. Sie ermöglichen es, Datenströme deklarativ zu transformieren, zu filtern und zu kombinieren. Jeder Operator gibt einen neuen Publisher zurück, sodass sie sich verketten lassen.
Wesentliche Transformations-Operators
Transformations-Operators verändern jeden emittierten Wert. map transformiert Werte, flatMap flacht verschachtelte Publisher ab und compactMap filtert nil-Werte heraus:
import Combine
var cancellables = Set<AnyCancellable>()
// map: transforms each value
// Equivalent to map on arrays
[1, 2, 3, 4, 5].publisher
.map { $0 * 2 } // Multiply each number by 2
.sink { print("Doubled: \($0)") }
.store(in: &cancellables)
// Output: 2, 4, 6, 8, 10
// compactMap: transforms AND filters out nil
// Useful for optional conversions
["1", "two", "3", "four", "5"].publisher
.compactMap { Int($0) } // Convert to Int, ignore failures
.sink { print("Valid number: \($0)") }
.store(in: &cancellables)
// Output: 1, 3, 5
// flatMap: flattens nested Publishers
// Essential for chaining async operations
struct User { let id: Int; let name: String }
func fetchUser(id: Int) -> AnyPublisher<User, Never> {
// Simulate an API call
Just(User(id: id, name: "User \(id)"))
.delay(for: .milliseconds(100), scheduler: RunLoop.main)
.eraseToAnyPublisher()
}
[1, 2, 3].publisher
.flatMap { id in fetchUser(id: id) } // Each ID becomes an API call
.sink { user in print("User: \(user.name)") }
.store(in: &cancellables)Filter-Operators
Filter-Operators steuern, welche Werte die Pipeline durchlaufen. Sie sind unverzichtbar, um unnötige Verarbeitung zu vermeiden und die Performance zu optimieren:
import Combine
var cancellables = Set<AnyCancellable>()
let numbers = [1, 2, 2, 3, 3, 3, 4, 5, 5].publisher
// filter: keeps only values that satisfy the condition
numbers
.filter { $0 > 2 } // Keep only numbers > 2
.sink { print("Filtered: \($0)") }
.store(in: &cancellables)
// Output: 3, 3, 3, 4, 5, 5
// removeDuplicates: removes consecutive identical values
numbers
.removeDuplicates() // Eliminate consecutive duplicates
.sink { print("Without duplicates: \($0)") }
.store(in: &cancellables)
// Output: 1, 2, 3, 4, 5
// debounce: waits for a pause before emitting
// Perfect for real-time search
let searchText = PassthroughSubject<String, Never>()
searchText
.debounce(for: .milliseconds(300), scheduler: RunLoop.main)
.removeDuplicates() // Ignore if text hasn't changed
.sink { query in
print("Search: \(query)")
// Launch API call here
}
.store(in: &cancellables)
// Simulate rapid typing
searchText.send("S")
searchText.send("Sw")
searchText.send("Swi")
searchText.send("Swift") // Only "Swift" is emitted after 300msBereit für deine iOS-Interviews?
Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.
Mehrere Publisher kombinieren
Reale Anwendungen müssen häufig mehrere Datenquellen verbinden. Combine bietet verschiedene Operators, um diese Mehrfachströme zu orchestrieren.
CombineLatest und Zip
combineLatest emittiert, sobald irgendein Publisher emittiert, und kombiniert dabei mit den letzten Werten der anderen. zip wartet, bis alle Publisher emittiert haben, bevor kombiniert wird:
import Combine
var cancellables = Set<AnyCancellable>()
// Simulate a form with validation
let email = CurrentValueSubject<String, Never>("")
let password = CurrentValueSubject<String, Never>("")
// combineLatest: combines the latest values from each Publisher
// Emits on every change from either source
Publishers.CombineLatest(email, password)
.map { email, password in
// Validate that email contains @ and password > 6 chars
let isEmailValid = email.contains("@")
let isPasswordValid = password.count >= 6
return isEmailValid && isPasswordValid
}
.sink { isFormValid in
print("Form valid: \(isFormValid)")
}
.store(in: &cancellables)
email.send("user@example.com") // false (password empty)
password.send("123456") // true (both are valid)
// zip: waits for one value from each Publisher before emitting
// Useful for synchronizing parallel operations
let firstAPI = PassthroughSubject<String, Never>()
let secondAPI = PassthroughSubject<Int, Never>()
Publishers.Zip(firstAPI, secondAPI)
.sink { stringValue, intValue in
print("Received pair: \(stringValue), \(intValue)")
}
.store(in: &cancellables)
firstAPI.send("Hello") // No emission, waiting for secondAPI
secondAPI.send(42) // Emits: ("Hello", 42)
firstAPI.send("World") // No emission, waiting for secondAPI
secondAPI.send(100) // Emits: ("World", 100)Merge zur Vereinheitlichung von Strömen
merge führt mehrere Publisher desselben Typs zu einem einzigen Strom zusammen. Werte treffen in der Reihenfolge ihrer Emission ein, unabhängig vom Absender:
import Combine
var cancellables = Set<AnyCancellable>()
// Multiple user notification sources
let pushNotifications = PassthroughSubject<String, Never>()
let localNotifications = PassthroughSubject<String, Never>()
let inAppMessages = PassthroughSubject<String, Never>()
// Merge unifies all streams into one
Publishers.Merge3(pushNotifications, localNotifications, inAppMessages)
.sink { message in
// Handle all notifications the same way
print("📬 Notification: \(message)")
}
.store(in: &cancellables)
pushNotifications.send("New message") // 📬 Notification: New message
localNotifications.send("Reminder: meeting") // 📬 Notification: Reminder: meeting
inAppMessages.send("Welcome!") // 📬 Notification: Welcome!Fehlerbehandlung in Combine
Fehlerbehandlung ist fest in den Kern von Combine integriert. Der Failure-Typ der Publisher erlaubt es dem Compiler zu prüfen, dass alle Fehler behandelt werden.
Recovery-Strategien
Combine bietet mehrere Operators für die Fehlerbehandlung: catch, um durch einen anderen Publisher zu ersetzen, retry, um es erneut zu versuchen, und replaceError für einen Standardwert:
import Combine
var cancellables = Set<AnyCancellable>()
enum APIError: Error {
case networkError
case invalidResponse
case serverError(Int)
}
// Simulate an API call that can fail
func fetchData() -> AnyPublisher<String, APIError> {
Fail(error: APIError.networkError)
.eraseToAnyPublisher()
}
// retry: retries N times before propagating the error
fetchData()
.retry(3) // Try up to 3 times
.catch { error -> Just<String> in
// catch: replaces the error with a fallback Publisher
print("Error after 3 attempts: \(error)")
return Just("Cached data") // Fallback value
}
.sink(
receiveCompletion: { _ in },
receiveValue: { print("Result: \($0)") }
)
.store(in: &cancellables)
// replaceError: replaces any error with a fixed value
// Simpler than catch when only a default value is needed
fetchData()
.replaceError(with: "Error - default value")
.sink { print("With fallback: \($0)") }
.store(in: &cancellables)Mit setFailureType(to:) lässt sich ein Never-Publisher in einen umwandeln, der scheitern kann; replaceError(with:) oder catch machen den umgekehrten Weg.
MVVM-Muster mit Combine
Combine fügt sich natürlich in das MVVM-Muster (Model-View-ViewModel) ein. Das ViewModel stellt Publisher bereit, die die View beobachtet, und schafft so eine reaktive Bindung zwischen Daten und Oberfläche.
Vollständiges reaktives ViewModel
Hier ein Beispiel-ViewModel für eine Benutzerliste mit Suche, Ladezustand und Fehlerbehandlung:
import Combine
import Foundation
// Data model
struct User: Codable, Identifiable {
let id: Int
let name: String
let email: String
}
// ViewModel with reactive state
final class UserListViewModel: ObservableObject {
// MARK: - Published Properties (observed by SwiftUI)
@Published var users: [User] = [] // User list
@Published var searchQuery: String = "" // Search text
@Published var isLoading: Bool = false // Loading state
@Published var errorMessage: String? // Optional error message
// MARK: - Private Properties
private var cancellables = Set<AnyCancellable>()
private let userService: UserServiceProtocol
// MARK: - Computed Properties
// Filters users based on search query
var filteredUsers: [User] {
guard !searchQuery.isEmpty else { return users }
return users.filter {
$0.name.localizedCaseInsensitiveContains(searchQuery)
}
}
// MARK: - Initialization
init(userService: UserServiceProtocol = UserService()) {
self.userService = userService
setupBindings()
}
// MARK: - Private Methods
private func setupBindings() {
// Observe searchQuery changes
// debounce prevents too frequent calls
$searchQuery
.debounce(for: .milliseconds(300), scheduler: RunLoop.main)
.removeDuplicates()
.sink { [weak self] query in
// Server-side search logic if needed
print("Search updated: \(query)")
}
.store(in: &cancellables)
}
// MARK: - Public Methods
func loadUsers() {
isLoading = true
errorMessage = nil
userService.fetchUsers()
.receive(on: DispatchQueue.main) // Ensure UI updates on main thread
.sink(
receiveCompletion: { [weak self] completion in
self?.isLoading = false
if case .failure(let error) = completion {
self?.errorMessage = error.localizedDescription
}
},
receiveValue: { [weak self] users in
self?.users = users
}
)
.store(in: &cancellables)
}
}Service mit Combine und URLSession
URLSession integriert Combine nativ über dataTaskPublisher. So lässt sich ein wiederverwendbarer Netzwerkdienst aufbauen:
import Combine
import Foundation
protocol UserServiceProtocol {
func fetchUsers() -> AnyPublisher<[User], Error>
}
final class UserService: UserServiceProtocol {
private let baseURL = URL(string: "https://api.example.com")!
private let session: URLSession
private let decoder: JSONDecoder
init(session: URLSession = .shared) {
self.session = session
self.decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
}
func fetchUsers() -> AnyPublisher<[User], Error> {
let url = baseURL.appendingPathComponent("users")
return session.dataTaskPublisher(for: url)
// Check HTTP status code
.tryMap { data, response in
guard let httpResponse = response as? HTTPURLResponse else {
throw URLError(.badServerResponse)
}
guard 200..<300 ~= httpResponse.statusCode else {
throw URLError(.badServerResponse)
}
return data
}
// Decode JSON to Swift model
.decode(type: [User].self, decoder: decoder)
// Erase concrete type to return AnyPublisher
.eraseToAnyPublisher()
}
}Bereit für deine iOS-Interviews?
Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.
SwiftUI-Integration
Combine und SwiftUI bilden ein starkes Duo. Die @Published-Properties eines ObservableObject lösen automatisch View-Updates aus.
SwiftUI-View mit Combine-ViewModel
So wird das ViewModel an eine SwiftUI-View angebunden:
import SwiftUI
struct UserListView: View {
// StateObject: creates and owns the ViewModel
@StateObject private var viewModel = UserListViewModel()
var body: some View {
NavigationStack {
Group {
if viewModel.isLoading {
// Centered loading indicator
ProgressView("Loading...")
} else if let error = viewModel.errorMessage {
// Error view with retry button
VStack(spacing: 16) {
Text("Error: \(error)")
.foregroundStyle(.red)
Button("Retry") {
viewModel.loadUsers()
}
}
} else {
// User list
List(viewModel.filteredUsers) { user in
UserRowView(user: user)
}
}
}
.navigationTitle("Users")
.searchable(text: $viewModel.searchQuery) // Direct binding
.onAppear {
viewModel.loadUsers() // Load on first appearance
}
}
}
}
struct UserRowView: View {
let user: User
var body: some View {
VStack(alignment: .leading, spacing: 4) {
Text(user.name)
.font(.headline)
Text(user.email)
.font(.subheadline)
.foregroundStyle(.secondary)
}
.padding(.vertical, 4)
}
}Fortgeschrittene Muster
Cancellation und automatische Bereinigung
Das Lifecycle-Management von Subscriptions ist entscheidend, um Memory Leaks zu vermeiden. Das cancellables-Muster mit AnyCancellable sorgt für automatische Bereinigung:
import Combine
final class DataManager {
// Set of cancellables: automatically cancelled on destruction
private var cancellables = Set<AnyCancellable>()
// Individual cancellable for fine-grained control
private var currentRequest: AnyCancellable?
func startPolling() {
// Timer that emits every 5 seconds
Timer.publish(every: 5, on: .main, in: .common)
.autoconnect() // Starts automatically
.sink { [weak self] _ in
self?.fetchLatestData()
}
.store(in: &cancellables)
}
func fetchLatestData() {
// Cancel the previous request if it exists
currentRequest?.cancel()
currentRequest = URLSession.shared
.dataTaskPublisher(for: URL(string: "https://api.example.com/data")!)
.map(\.data)
.decode(type: [String].self, decoder: JSONDecoder())
.replaceError(with: [])
.receive(on: DispatchQueue.main)
.sink { data in
print("Data received: \(data)")
}
}
deinit {
// All cancellables are automatically cancelled
print("DataManager destroyed, subscriptions cancelled")
}
}Schedulers für Threading
Schedulers steuern, in welchem Thread Operationen ausgeführt werden. subscribe(on:) für Hintergrundarbeit und receive(on:) für UI-Updates:
import Combine
import Foundation
var cancellables = Set<AnyCancellable>()
func loadAndProcessData() -> AnyPublisher<ProcessedData, Error> {
URLSession.shared.dataTaskPublisher(for: apiURL)
// Perform parsing on a background thread
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.map(\.data)
.decode(type: RawData.self, decoder: JSONDecoder())
// Heavy processing on background thread
.map { rawData in
// This expensive operation runs in the background
processData(rawData)
}
// Return to main thread for UI
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}Fazit
Combine bietet einen mächtigen, deklarativen Ansatz, um asynchrone Datenströme in iOS-Apps zu beherrschen. Die wichtigsten Punkte:
✅ Publishers emittieren Werte über die Zeit
✅ Subscribers empfangen und verarbeiten diese Werte
✅ Operators transformieren und kombinieren Ströme
✅ AnyCancellable verwaltet den Subscription-Lifecycle
✅ @Published mit SwiftUI erzeugt automatische reaktive Bindings
✅ Schedulers steuern das Threading für optimale Performance
Wer Combine beherrscht, baut robuste, wartbare und reaktive iOS-Apps. Die native Integration mit SwiftUI macht es zu einem unverzichtbaren Werkzeug der modernen iOS-Entwicklung.
Fang an zu üben!
Teste dein Wissen mit unseren Interview-Simulatoren und technischen Tests.
Tags
Teilen
Verwandte Artikel

Combine vs async/await in Swift: Progressive Migrationsmuster
Vollständiger Leitfaden zur Migration von Combine zu async/await in Swift: progressive Strategien, Bridging-Muster und Paradigmen-Koexistenz in iOS-Codebasen.

iOS-Accessibility-Interviewfragen 2026: VoiceOver und Dynamic Type
Vorbereitung auf iOS-Interviews mit zentralen Accessibility-Fragen: VoiceOver, Dynamic Type, semantische Traits und Audits.

Swift Macros: praktische Beispiele für Metaprogrammierung
Vollständiger Leitfaden zu Swift Macros: Erstellung freistehender und attached Makros, AST-Manipulation mit swift-syntax und praktische Beispiele zur Vermeidung von Boilerplate-Code.