Combine for Dummies: Part 2 — Advanced Techniques and Beyond

Edoardo Troianiello
3 min readAug 4, 2023

--

Welcome to Combine for Dummies, If you’ve joined us just now, this is the sequel to our initial exploration of Swift’s Combine framework. In Part 1, we laid down the groundwork, understanding the basics of publishers, subscribers, and the beauty of transforming data streams. As we venture further, Part 2 will elevate our journey, diving into more intricate aspects and advanced techniques of Combine. So, buckle up and let’s continue our reactive programming adventure!

Error Handling in Combine

Mistakes happen, whether it’s an unreliable network or unexpected data. Thankfully, Combine provides us with tools to handle these gracefully.

  • catch: It allows you to provide a replacement or backup publisher when an error occurs. Think of it as a safety net.
URLSession.shared.dataTaskPublisher(for: url)
.catch { _ in
return Just(Data()) // Supply default data if network call fails
}
.sink(receiveCompletion: { completion in
switch completion {
case .finished: break
case .failure(let error): print("Error occurred: \(error)")
}
}, receiveValue: { data in
print("Received data: \(data)")
})
  • retry: If at first, you don’t succeed, retry gives you a do-over. Useful when, for instance, network requests fail due to intermittent issues.
URLSession.shared.dataTaskPublisher(for: url)
.retry(3) // If an error occurs, it will try again up to 3 times.
.sink(receiveCompletion: { completion in ... }, receiveValue: { data in ... })
  • replaceError: When something goes wrong, this replaces the error with a default value, ensuring the chain doesn’t break.
let numbersWithError: AnyPublisher<Int, Never> = [1, 2, 3, 4, nil].publisher
.replaceError(with: 0)
.eraseToAnyPublisher()

Subjects and Shared Publishers

  • Subjects: These are both a Publisher and a Subscriber. They’re the bridge in cases where you need more control over the data being emitted. Combine offers several subjects like PassthroughSubject and CurrentValueSubject.
let passthrough = PassthroughSubject<Int, Never>()

passthrough.sink { print($0) } // Subscribing
passthrough.send(42) // Sending a value - this will print 42

let currentValue = CurrentValueSubject<Int, Never>(0)
print(currentValue.value) // Prints 0
currentValue.value = 99 // Updates the value and notifies subscribers
  • Shared Publishers: There are scenarios where multiple subscribers might be interested in the same data, but you only want one subscription to the source publisher. share() ensures that one subscription is used, and all subscribers get the same data.
let sharedPublisher = URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.share()

// Both subscribers below will get data from a single network call.
sharedPublisher.sink { data in ... }
sharedPublisher.sink { data in ... }

Backpressure Management with Combine

In the world of reactive programming, backpressure refers to when a Publisher is emitting values faster than a Subscriber can handle them. With Combine, you can manage this using several techniques:

  • Debounce: Imagine you have a search bar, and you don’t want to hit the server with every keystroke, but rather wait until the user pauses typing:
searchField.publisher(for: \.text)
.debounce(for: 0.5, scheduler: DispatchQueue.main)
.sink { searchText in
// Call API with searchText
}
  • Throttle: This ensures that events are only sent at a specified interval, even if many are generated quickly.
slider.publisher(for: \.value)
.throttle(for: 1.0, scheduler: DispatchQueue.main)
.sink { sliderValue in
// Do something with sliderValue
}

Combining Multiple Publishers

Sometimes, you need to work with values from more than one Publisher. Combine offers methods to do this elegantly:

  • CombineLatest: This reacts whenever any of the combined Publishers emit a new value.
let namePublisher = ...  // emits names
let agePublisher = ... // emits ages

Publishers.CombineLatest(namePublisher, agePublisher)
.sink { name, age in
print("Name is \(name) and age is \(age)")
}
  • Zip: Unlike CombineLatest, zip waits until each Publisher emits a value. If you have two sequences, it pairs the first of each, then the second of each, and so on.
let userIDs = ...  // emits user IDs
let scores = ... // emits scores

Publishers.Zip(userIDs, scores)
.sink { userID, score in
print("User \(userID) has a score of \(score)")
}

10. Debugging in Combine

Combine provides tools to make debugging your reactive code more manageable:

  • Print: This operator logs every event from a Publisher:
numbers
.print("Number Stream")
.sink { print($0) }
  • Handle Events: This lets you peek into different stages of the Publisher-Subscriber lifecycle:
numbers
.handleEvents(receiveSubscription: { _ in
print("Subscription received!")
}, receiveOutput: { num in
print("Received number: \(num)")
}, receiveCompletion: { _ in
print("All done!")
}, receiveCancel: {
print("Cancelled")
})
.sink { _ in }

Conclusion

By now, you’re not just familiar with the basics of Combine but also with some advanced topics. As with any powerful tool, there’s always more to explore. Keep diving deeper, practice with small projects, and before you know it, you’ll be a Combine pro!

--

--

Edoardo Troianiello

Computer Engineer | iOS Developer | Alumni @Apple Developer Academy in Naples