Combine Framework: āļāļēāļĢāđāļāļĩāļĒāļāđāļāļĢāđāļāļĢāļĄāđāļāļīāļāļĢāļĩāđāļāļāļāļĩāļāđāļ Swift
āđāļāļĩāđāļĒāļ§āļāļēāļ Combine āļŠāļģāļŦāļĢāļąāļāļāļēāļĢāļāļąāļāļāļēāļĢāļŠāļāļĢāļĩāļĄāļāđāļāļĄāļđāļĨāđāļāļ asynchronous āđāļ Swift: Publishers, Subscribers, Operators āđāļĨāļ°āļĢāļđāļāđāļāļāļāļąāđāļāļŠāļđāļāļŠāļģāļŦāļĢāļąāļāđāļāļ iOS

āļāļēāļĢāđāļāļĩāļĒāļāđāļāļĢāđāļāļĢāļĄāđāļāļīāļāļĢāļĩāđāļāļāļāļĩāļāđāļāļĨāļĩāđāļĒāļāļ§āļīāļāļĩāļāļēāļĢāļāļąāļāļāļēāļĢāđāļŦāļāļļāļāļēāļĢāļāđāđāļāļ asynchronous āđāļĨāļ°āļŠāļāļĢāļĩāļĄāļāđāļāļĄāļđāļĨāđāļāđāļāļ iOS āļāļĒāđāļēāļāļŠāļīāđāļāđāļāļīāļ Combine āļāļķāđāļāđāļāđāļāđāļāļĢāļĄāđāļ§āļīāļĢāđāļāļāļąāđāļāđāļāļīāļĄāļāļāļ Apple āļāļģāđāļŠāļāļāđāļāļ§āļāļēāļāđāļāļ declarative āđāļĨāļ°āļāļĨāļāļāļ āļąāļĒāļāļēāļāļāļāļīāļāļāđāļāļĄāļđāļĨ āđāļāļ·āđāļāļāļąāļāļ§āļēāļāđāļāļāđāđāļĨāļāđāļāđāļāļĄāļđāļĨāļāļĩāđāļāļąāļāļāđāļāļ āļāļđāđāļĄāļ·āļāļāļĩāđāļāļēāļāļđāđāļāđāļēāļāļāļēāļāđāļāļ§āļāļīāļāļāļ·āđāļāļāļēāļāđāļāļāļāļāļķāļāļĢāļđāļāđāļāļāļāļĩāđāļāļĢāđāļāļĄāđāļāđāļāļēāļāļāļĢāļīāļ
Combine āļāļīāļāļāļąāđāļāļĄāļēāļāļĢāđāļāļĄāļāļąāļ iOS 13+ āđāļŦāđāļāļĢāļ°āļŠāļīāļāļāļīāļ āļēāļāļāļĩāļāļ§āđāļēāļāđāļ§āļĒāļāļēāļĢāļāļĢāļąāļāđāļāđāļāļāļāļ Apple āđāļĨāļ°āđāļāļ·āđāļāļĄāļāđāļāļāļąāļ SwiftUI āđāļāđāļāļĒāđāļēāļāđāļĢāđāļĢāļāļĒāļāđāļ āđāļĄāđāļāđāļāļāļāļđāđāļĨ dependency āļ āļēāļĒāļāļāļ
āđāļāļ§āļāļīāļāļŦāļĨāļąāļāļāļāļ Combine
Combine āļŠāļĢāđāļēāļāļāļķāđāļāļāļāđāļāļ§āļāļīāļāļŠāļģāļāļąāļāļŠāļēāļĄāļāļĒāđāļēāļ: Publishers āļāļĩāđāļŠāđāļāļāđāļē, Subscribers āļāļĩāđāļĢāļąāļāļāđāļē āđāļĨāļ° Operators āļāļĩāđāđāļāļĨāļāļāđāļāļĄāļđāļĨāļĢāļ°āļŦāļ§āđāļēāļāļāļąāđāļāļŠāļāļ āļŠāļāļēāļāļąāļāļĒāļāļĢāļĢāļĄāļāļĩāđāļāđāļ§āļĒāđāļŦāđāļŠāļĢāđāļēāļāđāļāļāđāđāļĨāļāđāļāđāļāļĄāļđāļĨāđāļāļāļĢāļĩāđāļāļāļāļĩāļāļāļĩāđāļāļĢāļ°āļāļāļāđāļāđāļēāļāđāļ§āļĒāļāļąāļāđāļāđ
Publisher: āđāļŦāļĨāđāļāļāđāļāļĄāļđāļĨ
Publisher āļāļ·āļāļāļāļīāļāļāđāļāļĄāļđāļĨāļāļĩāđāļŠāļēāļĄāļēāļĢāļāļŠāđāļāļĨāļģāļāļąāļāļāļāļāļāđāļēāļāļāļāļĄāļēāļāļēāļĄāđāļ§āļĨāļēāđāļāđ Publisher āđāļāđāļĨāļ°āļāļąāļ§āļāļ°āļāļĢāļ°āļāļēāļĻāļāļāļīāļāļāļĩāđāđāļāļĩāđāļĒāļ§āļāđāļāļāļŠāļāļāļāļāļīāļ: āļāļāļīāļāļāļāļāļāđāļēāļāļĩāđāļŠāđāļāļāļāļ (Output) āđāļĨāļ°āļāļāļīāļāļāļāļāļāđāļāļāļīāļāļāļĨāļēāļāļāļĩāđāļāļēāļāđāļāļīāļāļāļķāđāļ (Failure) āļāđāļāđāļāļāļĩāđāļāļ·āļāļ§āļīāļāļĩāļŠāļĢāđāļēāļ Publisher āđāļāđāļĨāļ°āļāļĢāļ°āđāļ āļ:
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"))
}
}āļāļāļīāļ Never āļŠāļģāļŦāļĢāļąāļ error āļŦāļĄāļēāļĒāļāļ§āļēāļĄāļ§āđāļē Publisher āļāļ°āđāļĄāđāļĄāļĩāļ§āļąāļāļĨāđāļĄāđāļŦāļĨāļ§ āļāļķāđāļāđāļāđāļāļāļēāļĢāļĢāļąāļāļāļĢāļ°āļāļąāļ āļ āļāļāļāļāļāļĄāđāļāļĨāđāļāļĩāđāļāđāļ§āļĒāļāļģāđāļŦāđāđāļāđāļāļāļąāļāļāļēāļĢ error āđāļĢāļĩāļĒāļāļāđāļēāļĒāļāļķāđāļ
Subscriber: āļĢāļąāļāļāđāļē
Subscriber āļāļ·āļāļāļđāđāļŠāļĄāļąāļāļĢāļĢāļąāļāļāđāļēāļāļēāļ Publisher āļ§āļīāļāļĩāļāļĩāđāļāļīāļĒāļĄāļāļĩāđāļŠāļļāļāđāļāļāļēāļĢāļŠāļĢāđāļēāļ Subscriber āļāļ·āļāđāļĄāļāļāļ sink āļāļķāđāļāļĢāļąāļ closure āļŠāļāļāļāļąāļ§: āļāļąāļ§āļŦāļāļķāđāļāļŠāļģāļŦāļĢāļąāļ error āļŦāļĢāļ·āļāļāļēāļĢāļŠāļīāđāļāļŠāļļāļ āđāļĨāļ°āļāļĩāļāļāļąāļ§āļŠāļģāļŦāļĢāļąāļāļāđāļēāļāļļāļāļāđāļēāļāļĩāđāļĢāļąāļāđāļāđāļēāļĄāļē:
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 successfullyāļāđāļāļāđāļāđāļ AnyCancellable āļāļĩāđ sink() āļāļ·āļāļāļĨāļąāļāļĄāļēāđāļŠāļĄāļ āļŦāļēāļāđāļĄāđāļĄāļĩāļāļēāļĢāļāđāļēāļāļāļīāļ āļāļēāļĢāļŠāļĄāļąāļāļĢāļāļ°āļāļđāļāļĒāļāđāļĨāļīāļāļāļąāļāļāļĩāđāļĨāļ°āļāļ°āđāļĄāđāđāļāđāļĢāļąāļāļāđāļēāđāļ āđ
āļāļēāļĢāđāļāļĨāļāļāđāļāļĄāļđāļĨāļāđāļ§āļĒ Operators
Operators āļāļ·āļāļŦāļąāļ§āđāļāļāļāļ Combine āļāļģāđāļŦāđāļŠāļēāļĄāļēāļĢāļāđāļāļĨāļ āļāļĢāļāļ āđāļĨāļ°āļĢāļ§āļĄāļŠāļāļĢāļĩāļĄāļāđāļāļĄāļđāļĨāđāļāđāđāļāļ declarative Operator āđāļāđāļĨāļ°āļāļąāļ§āļāļ·āļ Publisher āđāļŦāļĄāđāļāļāļāļĄāļē āļāļģāđāļŦāđāļŠāļēāļĄāļēāļĢāļāļāļģāļĄāļēāļāđāļāđāļāļ·āđāļāļĄāđāļāđāļāļĨāļģāļāļąāļāđāļāđ
Operators āđāļāļĨāļāļāđāļāļĄāļđāļĨāļāļĩāđāļāļģāđāļāđāļ
Operators āđāļāļĨāļāļāđāļāļĄāļđāļĨāļāļ°āļāļĢāļąāļāđāļāļĨāļĩāđāļĒāļāļāđāļēāļāļļāļāļāđāļēāļāļĩāđāļāļđāļāļŠāđāļāļāļāļ map āđāļāļĨāļāļāđāļē, flatMap āđāļāđ Publisher āļāļĩāđāļāđāļāļāļāļąāļ āđāļĨāļ° compactMap āļāļĢāļāļāļāđāļē nil āļāļāļ:
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)Operators āļāļĢāļāļāļāđāļāļĄāļđāļĨ
Operators āļāļĢāļāļāļāđāļāļĄāļđāļĨāļāļ§āļāļāļļāļĄāļ§āđāļēāļāđāļēāđāļāļāļ°āļāđāļēāļāđāļāđāļēāđāļāđāļāđāļāļāđāđāļĨāļāđ āļāļķāļāļŠāļģāļāļąāļāļāđāļāļāļēāļĢāđāļĨāļĩāđāļĒāļāļāļēāļĢāļāļĢāļ°āļĄāļ§āļĨāļāļĨāļāļĩāđāđāļĄāđāļāļģāđāļāđāļāđāļĨāļ°āđāļāļīāđāļĄāļāļĢāļ°āļŠāļīāļāļāļīāļ āļēāļ:
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 300msāļāļĢāđāļāļĄāļāļĩāđāļāļ°āļāļīāļāļīāļāļāļēāļĢāļŠāļąāļĄāļ āļēāļĐāļāđ iOS āđāļĨāđāļ§āļŦāļĢāļ·āļāļĒāļąāļāļāļĢāļąāļ?
āļāļķāļāļāļāļāđāļ§āļĒāļāļąāļ§āļāļģāļĨāļāļāđāļāļāđāļāđāļāļāļ, flashcards āđāļĨāļ°āđāļāļāļāļāļŠāļāļāđāļāļāļāļīāļāļāļĢāļąāļ
āļāļēāļĢāļĢāļ§āļĄ Publisher āļŦāļĨāļēāļĒāļāļąāļ§
āđāļāļāļāļĨāļīāđāļāļāļąāļāđāļāđāļĨāļāļāļĢāļīāļāļĄāļąāļāļāđāļāļāļĢāļ§āļĄāđāļŦāļĨāđāļāļāđāļāļĄāļđāļĨāļŦāļĨāļēāļĒāđāļŦāļĨāđāļāđāļāđāļēāļāđāļ§āļĒāļāļąāļ Combine āļĄāļĩ Operators āļŦāļĨāļēāļĒāļāļąāļ§āļŠāļģāļŦāļĢāļąāļāļāļąāļāļ§āļēāļāļŠāļāļĢāļĩāļĄāļŦāļĨāļēāļĒāđāļŠāđāļāļāļēāļāđāļŦāļĨāđāļēāļāļĩāđ
CombineLatest āđāļĨāļ° Zip
combineLatest āļāļ°āļŠāđāļāļāđāļēāļāļāļāļāļļāļāļāļĢāļąāđāļāļāļĩāđ Publisher āđāļ āđ āļŠāđāļāļāđāļēāļāļāļāļĄāļē āđāļāļĒāļĢāļ§āļĄāļāļąāļāļāđāļēāđāļŦāļĄāđāļĨāđāļēāļŠāļļāļāļāļāļāļāļąāļ§āļāļ·āđāļ āļŠāđāļ§āļ zip āļĢāļāļāļ Publisher āļāļļāļāļāļąāļ§āļŠāđāļāļāđāļēāļāļāļāļĄāļēāļāđāļāļāļāļķāļāļāļ°āļĢāļ§āļĄāļāđāļēāļāļąāļ:
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 āđāļāļ·āđāļāļĢāļ§āļĄāļŠāļāļĢāļĩāļĄāđāļāđāļāļŦāļāļķāđāļāđāļāļĩāļĒāļ§
merge āļĢāļ§āļĄ Publisher āļāļāļīāļāđāļāļĩāļĒāļ§āļāļąāļāļŦāļĨāļēāļĒāļāļąāļ§āđāļŦāđāđāļāđāļāļŠāļāļĢāļĩāļĄāđāļāļĩāļĒāļ§ āļāđāļēāļĄāļēāļāļķāļāļāļēāļĄāļĨāļģāļāļąāļāļāļēāļĢāļŠāđāļāđāļāļĒāđāļĄāđāļŠāļāđāļāļ§āđāļēāļĄāļēāļāļēāļ Publisher āļāļąāļ§āđāļ:
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!āļāļēāļĢāļāļąāļāļāļēāļĢāļāđāļāļāļīāļāļāļĨāļēāļāđāļ Combine
āļāļēāļĢāļāļąāļāļāļēāļĢāļāđāļāļāļīāļāļāļĨāļēāļāļāļđāļāļāļąāļāđāļ§āđāđāļāđāļāļāļāļĨāļēāļāļāļāļ Combine āļāļāļīāļ Failure āļāļāļ Publisher āļāđāļ§āļĒāđāļŦāđāļāļāļĄāđāļāđāļĨāļāļĢāđāļāļĢāļ§āļāļŠāļāļāđāļāđāļ§āđāļēāļāļļāļ error āļāļđāļāļāļąāļāļāļēāļĢāđāļĨāđāļ§
āļāļĨāļĒāļļāļāļāđāļāļēāļĢāļāļđāđāļāļ·āļ
Combine āļĄāļĩ Operators āļŦāļĨāļēāļĒāļāļąāļ§āļŠāļģāļŦāļĢāļąāļāļāļąāļāļāļēāļĢ error: catch āđāļāļ·āđāļāđāļāļāļāļĩāđāļāđāļ§āļĒ Publisher āļāļ·āđāļ, retry āđāļāļ·āđāļāļāļĒāļēāļĒāļēāļĄāļāļĩāļāļāļĢāļąāđāļ āđāļĨāļ° replaceError āļŠāļģāļŦāļĢāļąāļāļāđāļēāļāļąāđāļāļāđāļ:
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)āđāļāđ setFailureType(to:) āđāļāļ·āđāļāđāļāļĨāļ Publisher āļāļāļīāļ Never āđāļŦāđāļāļĨāļēāļĒāđāļāđāļāļāļāļīāļāļāļĩāđāļĨāđāļĄāđāļŦāļĨāļ§āđāļāđ āđāļĨāļ°āđāļāđ replaceError(with:) āļŦāļĢāļ·āļ catch āļŠāļģāļŦāļĢāļąāļāļāļīāļĻāļāļēāļāļāļĢāļāļāļąāļāļāđāļēāļĄ
āļĢāļđāļāđāļāļ MVVM āļāļąāļ Combine
Combine āļāļŠāļēāļāđāļāđāļēāļāļąāļāļĢāļđāļāđāļāļ MVVM (Model-View-ViewModel) āđāļāđāļāļĒāđāļēāļāđāļāđāļāļāļĢāļĢāļĄāļāļēāļāļī ViewModel āđāļāļīāļāđāļāļĒ Publisher āļāļĩāđ View āļāļāļĒāļŠāļąāļāđāļāļ āļāļģāđāļŦāđāđāļāļīāļāļāļēāļĢ binding āđāļāļāļĢāļĩāđāļāļāļāļĩāļāļĢāļ°āļŦāļ§āđāļēāļāļāđāļāļĄāļđāļĨāļāļąāļāļāļīāļāđāļāļāļĢāđāđāļāļ
ViewModel āđāļāļāļĢāļĩāđāļāļāļāļĩāļāļāļĢāļāļāđāļ§āļ
āļāļĩāđāļāļ·āļāļāļąāļ§āļāļĒāđāļēāļ ViewModel āļŠāļģāļŦāļĢāļąāļāļĢāļēāļĒāļāļēāļĢāļāļđāđāđāļāđāļāļĩāđāļĄāļĩāļāļēāļĢāļāđāļāļŦāļē āļāļēāļĢāđāļŦāļĨāļ āđāļĨāļ°āļāļēāļĢāļāļąāļāļāļēāļĢ error:
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 āļāđāļ§āļĒ Combine āđāļĨāļ° URLSession
URLSession āļāļāļ§āļ Combine āđāļ§āđāđāļāļāđāļāļāļĩāļāļāđāļēāļ dataTaskPublisher āļāļĩāđāļāļ·āļāļ§āļīāļāļĩāļŠāļĢāđāļēāļāđāļāļāļĢāđāļ§āļīāļŠāđāļāļĢāļ·āļāļāđāļēāļĒāļāļĩāđāļāļģāļāļĨāļąāļāļĄāļēāđāļāđāđāļāđ:
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()
}
}āļāļĢāđāļāļĄāļāļĩāđāļāļ°āļāļīāļāļīāļāļāļēāļĢāļŠāļąāļĄāļ āļēāļĐāļāđ iOS āđāļĨāđāļ§āļŦāļĢāļ·āļāļĒāļąāļāļāļĢāļąāļ?
āļāļķāļāļāļāļāđāļ§āļĒāļāļąāļ§āļāļģāļĨāļāļāđāļāļāđāļāđāļāļāļ, flashcards āđāļĨāļ°āđāļāļāļāļāļŠāļāļāđāļāļāļāļīāļāļāļĢāļąāļ
āļāļēāļĢāļāļģāļāļēāļāļĢāđāļ§āļĄāļāļąāļ SwiftUI
Combine āļāļąāļ SwiftUI āđāļāđāļāļāļđāđāļŦāļđāļāļĩāđāļāļĢāļāļāļĨāļąāļ āļāļļāļāļŠāļĄāļāļąāļāļī @Published āļāļāļ ObservableObject āļāļ°āļāļĢāļ°āļāļļāđāļāļāļēāļĢāļāļąāļāđāļāļ view āđāļāļĒāļāļąāļāđāļāļĄāļąāļāļī
View āļāļāļ SwiftUI āļāļąāļ ViewModel āļāļāļ Combine
āļāļĩāđāļāļ·āļāļ§āļīāļāļĩāđāļāļ·āđāļāļĄ ViewModel āđāļāđāļēāļāļąāļ view āļāļāļ SwiftUI:
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)
}
}āļĢāļđāļāđāļāļāļāļąāđāļāļŠāļđāļ
āļāļēāļĢāļĒāļāđāļĨāļīāļāđāļĨāļ°āļāļēāļĢāļāļģāļāļ§āļēāļĄāļŠāļ°āļāļēāļāļāļąāļāđāļāļĄāļąāļāļī
āļāļēāļĢāļāļąāļāļāļēāļĢāļ§āļāļāļĢāļāļĩāļ§āļīāļāļāļāļāļāļēāļĢāļŠāļĄāļąāļāļĢāđāļāđāļāļŠāļīāđāļāļŠāļģāļāļąāļāđāļāļ·āđāļāļŦāļĨāļĩāļāđāļĨāļĩāđāļĒāļ memory leak āļĢāļđāļāđāļāļ cancellables āļāļĩāđāđāļāđāļĢāđāļ§āļĄāļāļąāļ AnyCancellable āļĢāļąāļāļāļĢāļ°āļāļąāļāļāļēāļĢāļāļģāļāļ§āļēāļĄāļŠāļ°āļāļēāļāļāļąāļāđāļāļĄāļąāļāļī:
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 āļŠāļģāļŦāļĢāļąāļ threading
Schedulers āļāļ§āļāļāļļāļĄāļ§āđāļēāļāļēāļĢāļāļģāđāļāļīāļāļāļēāļĢāļāļ°āļāļđāļāļĢāļąāļāļāļ thread āđāļ āđāļāđ subscribe(on:) āļŠāļģāļŦāļĢāļąāļāļāļēāļāđāļāļ·āđāļāļāļŦāļĨāļąāļāđāļĨāļ° receive(on:) āļŠāļģāļŦāļĢāļąāļāļāļēāļĢāļāļąāļāđāļāļ UI:
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()
}āļāļāļŠāļĢāļļāļ
Combine āļĄāļāļāđāļāļ§āļāļēāļāļāļĩāđāļāļĢāļāļāļĨāļąāļāđāļĨāļ°āđāļāđāļāđāļāļ declarative āđāļāļāļēāļĢāļāļąāļāļāļēāļĢāļŠāļāļĢāļĩāļĄāļāđāļāļĄāļđāļĨ asynchronous āđāļāđāļāļ iOS āļāļĢāļ°āđāļāđāļāļŠāļģāļāļąāļ:
â
Publishers āļŠāđāļāļāđāļēāļāļāļāļĄāļēāļāļēāļĄāđāļ§āļĨāļē
â
Subscribers āļĢāļąāļāđāļĨāļ°āļāļĢāļ°āļĄāļ§āļĨāļāļĨāļāđāļēāđāļŦāļĨāđāļēāļāļąāđāļ
â
Operators āđāļāļĨāļāđāļĨāļ°āļĢāļ§āļĄāļŠāļāļĢāļĩāļĄāđāļāđāļēāļāđāļ§āļĒāļāļąāļ
â
AnyCancellable āļāļąāļāļāļēāļĢāļ§āļāļāļĢāļāļĩāļ§āļīāļāļāļāļāļāļēāļĢāļŠāļĄāļąāļāļĢ
â
@Published āļāļąāļ SwiftUI āļŠāļĢāđāļēāļāļāļēāļĢ binding āđāļāļāļĢāļĩāđāļāļāļāļĩāļāļāļąāļāđāļāļĄāļąāļāļī
â
Schedulers āļāļ§āļāļāļļāļĄ threading āđāļāļ·āđāļāđāļŦāđāđāļāđāļāļĢāļ°āļŠāļīāļāļāļīāļ āļēāļāļŠāļđāļāļŠāļļāļ
āļāļēāļĢāđāļāļĩāđāļĒāļ§āļāļēāļ Combine āđāļāļīāļāļāļēāļāļŠāļđāđāļāļēāļĢāļŠāļĢāđāļēāļāđāļāļ iOS āļāļĩāđāđāļāđāļāđāļāļĢāđāļ āļāļđāđāļĨāļĢāļąāļāļĐāļēāļāđāļēāļĒ āđāļĨāļ°āļāļāļāļŠāļāļāļ āļāļēāļĢāļāļŠāļēāļāļĢāļ§āļĄāđāļāđāļēāļāļąāļ SwiftUI āđāļāļĢāļ°āļāļąāļāđāļāļāļĩāļāļāļģāđāļŦāđ Combine āļāļĨāļēāļĒāđāļāđāļāđāļāļĢāļ·āđāļāļāļĄāļ·āļāļŠāļģāļāļąāļāļŠāļģāļŦāļĢāļąāļāļāļēāļĢāļāļąāļāļāļē iOS āļŠāļĄāļąāļĒāđāļŦāļĄāđ
āđāļĢāļīāđāļĄāļāļķāļāļāđāļāļĄāđāļĨāļĒ!
āļāļāļŠāļāļāļāļ§āļēāļĄāļĢāļđāđāļāļāļāļāļļāļāļāđāļ§āļĒāļāļąāļ§āļāļģāļĨāļāļāļŠāļąāļĄāļ āļēāļĐāļāđāđāļĨāļ°āđāļāļāļāļāļŠāļāļāđāļāļāļāļīāļāļāļĢāļąāļ
āđāļāđāļ
āđāļāļĢāđ
āļāļāļāļ§āļēāļĄāļāļĩāđāđāļāļĩāđāļĒāļ§āļāđāļāļ

Combine vs async/await āđāļ Swift: āļĢāļđāļāđāļāļāļāļēāļĢāļĒāđāļēāļĒāļĢāļ°āļāļāđāļāļāļāđāļāļĒāđāļāđāļāļāđāļāļĒāđāļ
āļāļđāđāļĄāļ·āļāļāļāļąāļāļŠāļĄāļāļđāļĢāļāđāļŠāļģāļŦāļĢāļąāļāļāļēāļĢāļĒāđāļēāļĒāļāļēāļ Combine āđāļāļĒāļąāļ async/await āđāļ Swift: āļāļĨāļĒāļļāļāļāđāđāļāļāļāđāļāļĒāđāļāđāļāļāđāļāļĒāđāļ āļĢāļđāļāđāļāļāļāļēāļĢāđāļāļ·āđāļāļĄāđāļĒāļ āđāļĨāļ°āļāļēāļĢāļāļĒāļđāđāļĢāđāļ§āļĄāļāļąāļāļāļāļāļāļĢāļ°āļāļ§āļāļāļąāļĻāļāđāđāļāđāļāđāļāđāļāļŠ iOS

āļāļģāļāļēāļĄāļŠāļąāļĄāļ āļēāļĐāļāđāļāļēāļĢāđāļāđāļēāļāļķāļ iOS āđāļāļāļĩ 2026: VoiceOver āđāļĨāļ° Dynamic Type
āđāļāļĢāļĩāļĒāļĄāļāļąāļ§āļŠāļąāļĄāļ āļēāļĐāļāđ iOS āļāđāļ§āļĒāļāļģāļāļēāļĄāļŠāļģāļāļąāļāđāļĢāļ·āđāļāļāļāļēāļĢāđāļāđāļēāļāļķāļ: VoiceOver, Dynamic Type, traits āđāļāļīāļāļāļ§āļēāļĄāļŦāļĄāļēāļĒ āđāļĨāļ°āļāļēāļĢāļāļĢāļ§āļāļŠāļāļ.

Swift Macros: āļāļąāļ§āļāļĒāđāļēāļāđāļāļīāļāļāļāļīāļāļąāļāļīāļāļāļ metaprogramming
āļāļđāđāļĄāļ·āļāļāļāļąāļāļŠāļĄāļāļđāļĢāļāđāļāļāļ Swift Macros: āļāļēāļĢāļŠāļĢāđāļēāļāļĄāļēāđāļāļĢ freestanding āđāļĨāļ° attached, āļāļēāļĢāļāļąāļāļāļēāļĢ AST āļāđāļ§āļĒ swift-syntax āđāļĨāļ°āļāļąāļ§āļāļĒāđāļēāļāđāļāļīāļāļāļāļīāļāļąāļāļīāđāļāļ·āđāļāļāļģāļāļąāļāđāļāđāļāļāđāļģāļāļēāļ