Skip to content
All essays
iOSMarch 27, 202512 min

Swift Combine Framework: Asynchronous Programming

Manage asynchronous events declaratively with Combine. Publishers, subscribers, and operators.

Ü
Ümit Uz
Mobile & Full Stack Developer

Combine is Apple's framework introduced in 2019 for processing values over time declaratively. It brings reactive programming paradigm to Swift.

Core Concepts

Publisher

Object that emits values over time:

swift
import Combine class NewsProvider { func fetchNews() -> AnyPublisher<String, Never> { return Future { promise in // Asynchronous operation DispatchQueue.main.asyncAfter(deadline: .now() + 2) { promise(.success("Breaking news!")) } } .eraseToAnyPublisher() } }

Subscriber

Object that receives values from a publisher:

swift
let publisher = ["Apple", "Pear", "Banana"].publisher let subscriber = publisher.sink { value in print("Received: \(value)") }

Subjects

PassthroughSubject

swift
class MessageService { let messagePublisher = PassthroughSubject<String, Never>() func sendMessage(_ message: String) { messagePublisher.send(message) } } let service = MessageService() // Subscribe service.messagePublisher .sink { message in print("Message: \(message)") } // Send service.sendMessage("Hello!") service.sendMessage("Combine is awesome!")

CurrentValueSubject

swift
class UserSettings { let isDarkModeEnabled = CurrentValueSubject<Bool, Never>(false) func toggleTheme() { isDarkModeEnabled.send(!isDarkModeEnabled.value) } } let settings = UserSettings() // Receive current value and future values settings.isDarkModeEnabled .sink { isEnabled in print("Dark mode: \(isEnabled)") } // Read current value print("Current: \(settings.isDarkModeEnabled.value)")

Operators

map

swift
[1, 2, 3, 4, 5].publisher .map { $0 * 2 } .sink { doubled in print("\(doubled)") } // Output: 2, 4, 6, 8, 10

filter

swift
(1...10).publisher .filter { $0 % 2 == 0 } .sink { even in print("\(even)") } // Output: 2, 4, 6, 8, 10

debounce

swift
class SearchViewModel: ObservableObject { @Published var searchText: String = "" private var cancellables = Set<AnyCancellable>() init() { $searchText .debounce(for: .milliseconds(300), scheduler: RunLoop.main) .removeDuplicates() .sink { text in self.performSearch(text) } .store(in: &cancellables) } func performSearch(_ text: String) { print("Searching: \(text)") } }

flatMap

swift
struct User { let id: Int let name: String } struct Post { let id: Int let userId: Int let title: String } func fetchUser(id: Int) -> AnyPublisher<User, Error> { // User fetch operation return Just(User(id: id, name: "John")) .setFailureType(to: Error.self) .eraseToAnyPublisher() } func fetchPosts(userId: Int) -> AnyPublisher<[Post], Error> { // Posts fetch operation return Just([ Post(id: 1, userId: userId, title: "First post") ]) .setFailureType(to: Error.self) .eraseToAnyPublisher() } // Usage fetchUser(id: 1) .flatMap { user in fetchPosts(userId: user.id) .map { posts in (user, posts) } } .sink { completion in print("Completed: \(completion)") } receiveValue: { (user, posts) in print("\(user.name) has \(posts.count) posts") }

@Published Property Wrapper

swift
class ViewModel: ObservableObject { @Published var items: [String] = [] @Published var isLoading: Bool = false private var cancellables = Set<AnyCancellable>() init() { // Monitor changes $items .sink { items in print("Items changed: \(items.count)") } .store(in: &cancellables) } func loadItems() { isLoading = true // Async load Just(["Apple", "Pear", "Banana"]) .delay(for: .seconds(2), scheduler: RunLoop.main) .sink { [weak self] items in self?.items = items self?.isLoading = false } .store(in: &cancellables) } }

Error Handling

swift
enum NetworkError: Error { case invalidURL case requestFailed } func fetchData() -> AnyPublisher<Data, NetworkError> { return Future<Data, NetworkError> { promise in // Simulated network request let success = Bool.random() DispatchQueue.main.asyncAfter(deadline: .now() + 1) { if success { promise(.success(Data())) } else { promise(.failure(.requestFailed)) } } } .eraseToAnyPublisher() } // Error handling fetchData() .catch { error in print("Error: \(error)") return Just(Data()) } .sink { data in print("Data received: \(data.count) bytes") }

SwiftUI Integration

swift
struct SearchView: View { @StateObject private var viewModel = SearchViewModel() var body: some View { List(viewModel.results, id: \.self) { result in Text(result) } .searchable(text: $viewModel.searchText) .onAppear { viewModel.load() } } } class SearchViewModel: ObservableObject { @Published var searchText = "" @Published var results: [String] = [] private var cancellables = Set<AnyCancellable>() func load() { $searchText .debounce(for: .milliseconds(500), scheduler: RunLoop.main) .removeDuplicates() .flatMap { query in self.search(query) } .sink { completion in } receiveValue: { results in self.results = results } .store(in: &cancellables) } private func search(_ query: String) -> AnyPublisher<[String], Never> { // Search implementation return Just([]) .eraseToAnyPublisher() } }

Combine Best Practices

  1. 1Memory management: Properly manage cancellables
  2. 2Error handling: Keep error types consistent
  3. 3Threading: Use receive(on:) for thread management
  4. 4Debounce: Use debounce for user input
  5. 5Cleanup: Cancel subscriptions when view disappears

Combine integrates perfectly with SwiftUI for asynchronous programming!

Next essay
Swift Concurrency: async/await and Actor