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!

Related essays

Next essay
Swift Concurrency: async/await and Actor