Swift Combine Series: Part 4 — Advanced Operators and Custom Publishers

Vikram Kumar
4 min readJan 29, 2025

--

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. 🎩✨

Photo by Dollar Gill on Unsplash

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! 🚀

--

--

Vikram Kumar
Vikram Kumar

Written by Vikram Kumar

I am Vikram, a Senior iOS Developer at Matellio Inc. focused on writing clean and efficient code. Complex problem-solver with an analytical and driven mindset.

No responses yet