Swift Combine Series: Part 4 — Advanced Operators and Custom Publishers
Welcome to Part 4 of our Swift Combine journey! If you’ve made it this far, congrats — you’re officially a Combine explorer. Today, we’ll unravel some of the most powerful tools in Combine’s arsenal: flatMap, switchToLatest, and custom publishers. These operators take your reactive skills to the next level, making your code even more dynamic, flexible, and dare I say — magical. 🎩✨
Take a look at the earlier segments of the Swift Combine series:
Why Advanced Operators Matter
Imagine you’re juggling multiple API calls, handling real-time updates, or dealing with dependent network requests. Basic publishers might struggle, but flatMap and switchToLatest step in like superheroes to save the day. And if that’s not enough, you can even create custom publishers tailored to your needs. Let’s dive in!
Understanding flatMap
“When one stream just isn’t enough,
flatMap
comes to the rescue!" - Some Wise Developer
flatMap
is perfect when you need to transform values into a new publisher and merge the results. Think of it as handling multiple network requests dynamically.
Example: Fetching Related Data
import Combine
import Foundation
struct User: Decodable {
let id: Int
let name: String
}
struct Post: Decodable {
let id: Int
let title: String
let body: String
}
class UserViewModel: ObservableObject {
@Published var posts: [Post] = []
private var cancellables = Set<AnyCancellable>()
func fetchUserAndPosts() {
let userPublisher = URLSession.shared.dataTaskPublisher(for: URL(string: "https://jsonplaceholder.typicode.com/users/2")!)
.map { $0.data }
.decode(type: User.self, decoder: JSONDecoder())
.flatMap { user in self.fetchUserPosts(userID: user.id) } // Fetch posts after getting user
.sink(receiveCompletion: { print("Completed: \($0)") }, receiveValue: { [weak self] posts in
print(posts)
self?.posts = posts
})
userPublisher.store(in: &cancellables)
}
func fetchUserPosts(userID: Int) -> AnyPublisher<[Post], Error> {
let url = URL(string: "https://jsonplaceholder.typicode.com/posts?userId=\(userID)")!
return URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.decode(type: [Post].self, decoder: JSONDecoder())
.eraseToAnyPublisher()
}
}
Using the ViewModel
let viewModel = UserViewModel()
viewModel.fetchUserAndPosts()
What’s Happening Here?
- We first fetch a User from an API.
- Once we have the user ID, we flatMap it into a network call to fetch their posts.
- The result is beautifully chained together!
switchToLatest
: The King of Cancellation
“Out with the old, in with the new!” — Every
switchToLatest
operator ever
switchToLatest
is your go-to when you only care about the most recent request, canceling all previous ones. It’s perfect for search fields, live data updates, and real-time filtering.
Example: Real-Time Search Queries
let searchSubject = PassthroughSubject<String, Never>()
let searchCancellable = searchSubject
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
.map { query in URL(string: "https://api.example.com/search?query=\(query)")! }
.map { URLSession.shared.dataTaskPublisher(for: $0).map { $0.data }.eraseToAnyPublisher() }
.switchToLatest()
.sink(receiveCompletion: { print("Completed: \($0)") }, receiveValue: { print("Search Results: \($0)") })
searchSubject.send("Swift")
searchSubject.send("SwiftUI") // Cancels previous request, only SwiftUI search remains
Why switchToLatest
Rocks
- Ensures only the latest request matters.
- Great for search boxes where users type continuously.
- Prevents unnecessary API calls, improving performance.
Creating Custom Publishers
“If you can’t find what you need, build it yourself!” — Every developer at some point
Sometimes, built-in publishers don’t cut it. That’s when we create custom publishers tailored to specific needs.
Example: A Timer Publisher
struct CustomTimerPublisher: Publisher {
typealias Output = Date
typealias Failure = Never
let interval: TimeInterval
func receive<S>(subscriber: S) where S : Subscriber, Never == S.Failure, Date == S.Input {
let subscription = TimerSubscription(subscriber: subscriber, interval: interval)
subscriber.receive(subscription: subscription)
}
}
final class TimerSubscription<S: Subscriber>: Subscription where S.Input == Date, S.Failure == Never {
private var timer: Timer?
private var subscriber: S?
init(subscriber: S, interval: TimeInterval) {
self.subscriber = subscriber
self.timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { _ in
_ = subscriber.receive(Date())
}
}
func request(_ demand: Subscribers.Demand) {}
func cancel() { timer?.invalidate(); subscriber = nil }
}
Using Our Custom Publisher
let timerPublisher = CustomTimerPublisher(interval: 1.0)
.sink { print("Time: \($0)") }
Why Use Custom Publishers?
- Fine-tuned control over data streams.
- Great for specialized operations, like Bluetooth, timers, or sensors.
Final Thoughts
Combine is powerful, and these advanced operators make it even more versatile. flatMap, switchToLatest, and custom publishers help you craft elegant, efficient, and reactive Swift code. 🎉
“Coding is like magic, and Combine is your wand!” — Every Swift Developer Ever
In Part 5, we’ll explore Combine with CoreData and persistent storage. Get ready for some state management magic! See you soon! 🚀