/ COMBINE, SWIFT

Transforming Operators in Swift Combine Framework: Map vs FlatMap vs SwitchToLatest

map, flatMap and switchToLatest are the most important transforming operators of the Swift Combine Framework. Let’s learn how they work, what is the difference between them and when to use what.

If you want to freshen the basics, here is the bird-eye overview of the Combine Framework.

Transforming Elements with Map

Combine’s map(_:) operator works just like map from the Swift standard library, except it operates with publishers. It takes a closure that changes an element into another element:

[1, 2, 3]
   .publisher
   .map { $0 * 2 }
   .sink { print($0) }

It will print:

2
4
6

Transforming Publishers with FlatMap

The flatMap(maxPublishers:_:) operator transforms a publisher into completely new publisher that produces elements of the same type. It is used when you want to reach into inner publisher to get its elements.

To better understand the operator, let’s begin with an example. Say, we have a User struct with name property as a publisher:

struct User {
   let name: CurrentValueSubject<String, Never>
}

To print the names of the stream of users we create PassthroughSubject and send User objects to it:

let userSubject = PassthroughSubject<User, Never>()
        
userSubject
   .map { $0.name } // 🛑 Oops, compilation error here
   .sink { print($0) }

let user = User(name: .init("User 1"))
userSubject.send(user)

It will fail with compilation error, since userSubject is a publisher of publishers. The flatMap operator allows to overcome this and reach name values:

userSubject
   .flatMap { $0.name }
   .sink { print($0) }

It will print:

User 1

Controlling Number of Publishers with FlatMap

flatMap has maxPublishers parameter, that controls how many publishers the method can accept. It defaults to unlimited:

let anotherUser = User(name: .init("AnotherUser 1"))
userSubject.send(anotherUser)

anotherUser.name.send("AnotherUser 2")

user.name.send("User 2")

It will print names of both user and anotherUser:

User 1
AnotherUser 1
AnotherUser 2
User 2

Once we set maxPublishers to 1, flatMap accepts only user and ignores anotherUser:

userSubject
   .flatMap(maxPublishers: .max(1)) { $0.name }
   .sink { print($0) }

let user = User(name: .init("User 1"))
userSubject.send(user)

let anotherUser = User(name: .init("AnotherUser 1"))
userSubject.send(anotherUser)

anotherUser.name.send("AnotherUser 2")

user.name.send("User 2")

It will print:

User 1
User 2

Switching to Latest Publisher

As we’ve seen with flatMap, it’s very common to have a publisher of publishers. Say, firing a network request per button tap already creates such a stream. When requests are fired in a short period of time, their responses may arrive out-of-order. However, we are almost always interested in the latest network request. It appears that Combine has an operator that achieves exactly such behavior: switchToLatest().

Continuing with our example, let’s see how we can switch the stream to the latest publisher, i.e. anotherUser. The only part that changes is the subscription:

userSubject
   .map { $0.name }
   .switchToLatest()
   .sink { print($0) }

It will print:

User 1
AnotherUser 1
AnotherUser 2

Once anotherUser is inserted into the stream, userSubject automatically switches to it and no longer propagates values from user.

💡 If you have RxSwift or RxJava background, this is what flatMapLatest() does. In Combine it translates into map + switchToLatest.

Further Reading

If you enjoyed this article, I’ve been writing a lot about the Swift Combine Framework: