Skip to main content

Working with Combine Framework

1. Understanding Publishers and Subscribers

The Combine framework in iOS Swift provides a declarative approach to handling asynchronous events and data streams. At its core, it revolves around Publisher and Subscriber protocols.

Component Description Key Methods/Properties
Publisher Represents a stream of values that can be subscribed to. subscribe(_:), handleEvents
Subscriber Receives values from a Publisher. receive(subscription:), receive(_:)

Example: Basic Publisher and Subscriber Setup

import Combine

// Create a simple publisher
let publisher = PassthroughSubject<String, Never>()

// Create a subscriber
let subscriber = AnySubscriber<String, Never>(receiveCompletion: { completion in
    print("Completed with: " + String((describing: completion))
}, receiveValue: { value in
    print("Received: " + value)
})

// Subscribe the subscriber to the publisher
publisher.subscribe(subscriber)

// Send a value
publisher.send("Hello, Combine!")
Note: Publishers and Subscribers form the foundation of Combine's reactive programming model, enabling developers to chain operations and react to data changes efficiently.

2. Creating Publishers

Example: Creating a simple publisher from a value

import Combine

let publisher = Just("Hello, World!")
    .print("Just Publisher")

// Subscribing to the publisher
publisher
    .sink(
        receiveCompletion: { completion in
            print("Completed with: ", completion)
        },
        receiveValue: { value in
            print("Received value: ", value)
        }
    )
Method Description Return Type
Just<Output> Creates a publisher that emits a single value and finishes Publisher
PassthroughSubject<Output> Creates a subject that can emit values to subscribers Subject
CurrentValueSubject<Output> Creates a subject that holds and emits the current value Subject
Future<Output, Failure> Creates a publisher that completes with a single value or failure Publisher
Note: Just is useful for testing or providing default values in a stream.
Warning: PassthroughSubject does not hold a value, so subscribers must subscribe before values are emitted to receive them.

3. Using CurrentValueSubject

Property Value Description
CurrentValueSubject<Output, Failure> CurrentValueSubject<String, Never> Initializes a subject with a current value
send(_ value: Output) subject.send("New Value") Sends a new value to all subscribers
value subject.value Gets or sets the current value of the subject

Example: Creating and Using CurrentValueSubject

import Combine

let subject = CurrentValueSubject<String, Never>("Initial Value")

// Subscribe to the subject
let subscription = subject.sink { value in
    print("Received: \\(value)")
}

// Send a new value
subject.send("Updated Value")

// Change the current value
subject.value = "New Current Value"
Note: CurrentValueSubject is useful for representing state that can be observed and updated.

4. Subscribing with sink and assign

Example: Using sink to handle values and completion

import Combine

let subject = PassthroughSubject<String, Never>()

let cancellable = subject
  .sink(
    receiveValue: { value in
      print("Received: \\(value)")
    },
    receiveCompletion: { completion in
      switch completion {
      case .finished:
        print("Completed successfully")
      case .failure(let error):
        print("Error: \\(error)")
      }
    }
  )

subject.send("Hello")
subject.send("World")
subject.send(completion: .finished)
Method Description Parameters Return Type
sink(receiveValue:) Subscribes to a publisher and handles received values receiveValue: (Value) -> Void Cancellable
sink(receiveValue:receiveCompletion:) Subscribes to a publisher and handles values and completion receiveValue: (Value) -> Void, receiveCompletion: (Subscribers.Completion<Failure>) -> Void Cancellable
Note: The sink method is a terminal operator that starts the subscription and keeps it alive until the publisher finishes or is cancelled.

Example: Using assign to update a property

import Combine

class ViewModel {
  @Published var title = ""
}

let viewModel = ViewModel()
let subject = PassthroughSubject<String, Never>()

let cancellable = subject
  .assign(
    to: \.title,
    on: viewModel
  )

subject.send("New Title")
print(viewModel.title) // Output: "New Title"
Method Description Parameters Return Type
assign(to:) Assigns received values to a property of an object to: WritableKeyPath<Object, Value>, on: Object Cancellable
Note: The assign method is a terminal operator that assigns values to a property and keeps the subscription alive.

5. Using Operators

Example: Transforming Values with map

import Combine

let numbers = [1, 2, 3, 4, 5].publisher
let doubled = numbers.map { $0 * 2 }

doubled.sink(
    receiveCompletion: { print("Completed") },
    receiveValue: { value in
        print("Received: \\(value)")
    }
)
// Output:
// Received: 2
// Received: 4
// Received: 6
// Received: 8
// Received: 10
// Completed
Operator Description Example
map Transforms each element in the stream publisher.map { $0 * 2 }
filter Filters elements based on a condition publisher.filter { $0 > 5 }
compactMap Transforms and filters out nil values publisher.compactMap { $0?.description }
flatMap Flattens publishers from elements publisher.flatMap { $0.publisher }
scan Accumulates values over time publisher.scan(0) { $0 + $1 }
removeDuplicates Removes consecutive duplicate values publisher.removeDuplicates()
first Emits only the first value publisher.first()
last Emits only the last value publisher.last()
Note: Operators can be chained to create complex data transformations.

Example: Combining Publishers with merge

import Combine

let publisher1 = ["A", "B"].publisher
let publisher2 = ["C", "D"].publisher

let merged = publisher1.merge(with: publisher2)

merged.sink(
    receiveCompletion: { print("Completed") },
    receiveValue: { value in
        print("Received: \\(value)")
    }
)
// Output:
// Received: A
// Received: B
// Received: C
// Received: D
// Completed

Example: Combining Publishers with combineLatest

import Combine

let publisher1 = [1, 2].publisher
let publisher2 = ["X", "Y"].publisher

let combined = publisher1.combineLatest(publisher2)

combined.sink(
    receiveCompletion: { print("Completed") },
    receiveValue: { value in
        print("Received: \\(value)")
    }
)
// Output:
// Received: (1, "X")
// Received: (2, "X")
// Received: (2, "Y")
// Completed

6. Combining Publishers

Example: Merging Publishers

import Combine

let publisher1 = PassthroughSubject<Int, Never>()
let publisher2 = PassthroughSubject<Int, Never>()

let mergedPublisher = publisher1.merge(with: publisher2)

mergedPublisher
    .sink { value in
        print("Merged value: \\(value)")
    }
    .store(in: &bag)

publisher1.send(1)
publisher2.send(2)
publisher1.send(3)
Method Description Return Type
merge(with:) Merges elements from multiple publishers into a single stream Publisher
combineLatest(with:) Combines the latest values from two publishers Publisher
zip(with:) Zips values from two publishers into tuples Publisher
switchToLatest() Switches to the latest inner publisher Publisher
Note: merge(with:) does not preserve order from individual publishers.

Example: Combining Latest Values

import Combine

let publisher1 = PassthroughSubject<String, Never>()
let publisher2 = PassthroughSubject<Int, Never>()

let combinedPublisher = publisher1
    .combineLatest(with: publisher2)
    .map { string, number in
        return "\\(string) - \\(number)"
    }

combinedPublisher
    .sink { value in
        print("Combined: \\(value)")
    }
    .store(in: &bag)

publisher1.send("Hello")
publisher2.send(42)

7. Handling Errors in Combine

Error Type Description Handling Strategy
Error Generic Swift error type Use catch operator to handle and transform errors
URLError Errors specific to URL requests in Combine Use catch with URLError to handle network errors
DecodingError Errors during data decoding in Combine Use catch with DecodingError to handle JSON or data parsing errors

Example: Catching Errors in Combine

import Combine

let url = URL(string: "https://api.example.com/data")!
let publisher = URLSession.shared.dataTaskPublisher(for: url)
    .map(\.data)
    .decode(type: [String: Any].self, decoder: JSONDecoder())
    .mapError { error in
        if let urlError = error as? URLError {
            return "Network Error: \(urlError.localizedDescription)"
        } else if let decodingError = error as? DecodingError {
            return "Decoding Error: \(decodingError.localizedDescription)"
        } else {
            return "Unknown Error: \(error.localizedDescription)"
        }
    }
    .catch { error in
        Just(["error": error.localizedDescription])
            .setFailureType(to: Error.self)
    }
    .sink(
        receiveCompletion: { completion in
            switch completion {
            case .finished:
                print("Completed successfully")
            case .failure(let error):
                print("Failed with error: \(error)")
            }
        },
        receiveValue: { data in
            print("Received data: \(data)")
        }
    )
Note: The catch operator allows you to transform errors into values or handle them gracefully, ensuring your Combine pipeline can continue processing even when errors occur.

8. Managing Subscriptions

Example: Basic Subscription Management

import Combine

class MyViewModel: ObservableObject {
    @Published var data: String = ""
    private var cancellables = Set<AnyCancellable>()

    init() {
        // Subscribe to a publisher
        $data
            .sink { value in
                print("Received: \($0)")
            }
            .store(in: &cancellables)
    }
}
Method Description Usage
store(in:) Stores the subscription in a collection to manage its lifetime cancellable.store(in: &cancellables)
assign(to:) Assigns the latest value to a property publisher.assign(to: &object.property)
sink() Handles received values and completion publisher.sink(receiveValue: { value in ... })
Note: Always store subscriptions in a Set<AnyCancellable> to prevent memory leaks.
Warning: Failing to store subscriptions can lead to unexpected behavior or crashes.

9. Using Schedulers

Example: Configuring a Scheduler

import Combine

let scheduler = DispatchQueue.main
let publisher = PassthroughSubject<String, Never>()

publisher
    .receive(on: scheduler)
    .sink { value in
        print("Received on main queue: \\(value)")
    }
    .store(in: &bag)
Scheduler Description Usage
RunLoop Runs tasks on the main run loop RunLoop.main
DispatchQueue Runs tasks on a specific dispatch queue DispatchQueue.global()
OperationQueue Runs tasks on an operation queue OperationQueue()
Note: Using receive(on:) ensures that downstream operators and subscribers execute on the specified scheduler.
Warning: Avoid using DispatchQueue.main for heavy computation as it can block the UI thread.

Key Points

  • Schedulers control where and when Combine publishers and subscribers execute.
  • receive(on:) is used to specify the scheduler for downstream operations.
  • Use DispatchQueue.global() for background processing to avoid blocking the main thread.

10. Integrating Combine with SwiftUI

Example: Binding to a Published Property

import SwiftUI
import Combine

class DataModel: ObservableObject {
    @Published var userName = "John"
}

struct ContentView: View {
    @StateObject private var model = DataModel()
    
    var body: some View {
        TextField("Enter name", text: $model.userName)
    }
}
Property Type Description
@StateObject StateObject<T> Manages the lifecycle of a state object
@Published Published<T> Triggers view updates when value changes
Note: Use @StateObject for objects that manage state, and @Published to expose properties that should trigger view updates.

Example: Handling Asynchronous Events

import SwiftUI
import Combine

class NetworkService: ObservableObject {
    @Published var data: String = ""
    private var cancellables = Set<AnyCancellable>()
    
    func fetchData() {
        URLSession.shared.dataTaskPublisher(for: URL(string: "https://api.example.com/data")!)
            .map { $0.data }
            .decode(type: String.self, decoder: JSONDecoder())
            .receive(on: DispatchQueue.main)
            .sink(
                receiveCompletion: { _ in },
                receiveValue: { [weak self] value in
                    self?.data = value
                }
            )
            .store(in: &cancellables)
    }
}

struct ContentView: View {
    @StateObject private var networkService = NetworkService()
    
    var body: some View {
        Text(networkService.data)
            .onAppear {
                networkService.fetchData()
            }
    }
}

Was this article helpful?