Publishing property changes in Combine
Published on: January 27, 2020In Combine, everything is considered a stream of values that are emitted over time. This means that sometimes a publisher can publish many values, and other times it publishes only a single value. And other times it errors and publishes no values at all. When your UI has to respond to changing data, or if you want to update your UI in response to a user's actions, you might consider the data and user input to both be streams of values. When we looked at networking in my previous post, it was possible to use a built-in publisher that is specialized for networking. In my introduction to Combine I showed you that you can subscribe to NotificationCenter
notifications using another dedicated publisher.
There are no dedicated publishers for your own objects and properties. There are, however, publishers that allow you to publish objects of a certain type at will. These publishers are the PassthroughSubject
and CurrentValueSubject
publishers. Both of these publishers allow you to push values at will, but they have slightly different use cases.
Using a PassthroughSubject to publish values
The PassthroughSubject
publisher is used to send values to subscribers when they become available. It doesn't have a sense of state. This means that there is no current value, a PassthroughSubject
will pretty much take what you want to send on one end, and it comes out the other. Let's look at an example:
var stringSubject = PassthroughSubject<String, Never>()
stringSubject.sink(receiveValue: { value in
print("Received value: \(value)")
})
stringSubject.send("Hello") // prints: Received value: Hello
stringSubject.send("World") // prints: Received value: World
A publisher like this useful to send temporal information, like events. For example, I think that it's likely that the built-in NotificationCenter
publisher is implemented as PassthroughSubject
. Let me show you an example:
let notificationSubject = PassthroughSubject<Notification, Never>()
let notificationName = Notification.Name("MyNotification")
let center = NotificationCenter.default
center.addObserver(forName: notificationName, object: nil, queue: nil) { notification in
notificationSubject.send(notification)
}
notificationSubject.sink(receiveValue: { notification in
print("received notification: \(notification)")
})
center.post(name: notificationName, object: nil, userInfo: ["Hello": "World!"])
In this code snippet, I created a PassthroughSubject
that publishes Notification
objects. I added an observer to the default NotificationCenter
. In the closure that is executed for that observer when a notification is received, I tell my PassthroughSubject
to send the received notification object to its subscribers. Because I used sink
to subscribe to the PassthroughSubject
that I created, I will then receive any notifications that are posted after I subscribed to the PassthroughSubject
. Because a PassthroughSubject
doesn't expose any state, I don't have access to old notifications. Do you see why I think it's likely that Apple used a PassthroughSubject
or something similar to implement the built-in NotificationCenter
publisher?
Using a CurrentValueSubject to publish values
In many cases where you have a model that's used to drive your UI, you are interested in a concept of state. The model has a current value. It might be a default value or a user-provided value, but there always is a value. Let's consider the following simple example:
class Car {
var kwhInBattery = 50.0
let kwhPerKilometer = 0.14
func drive(kilometers: Double) {
var kwhNeeded = kilometers * kwhPerKilometer
assert(kwhNeeded <= kwhInBattery, "Can't make trip, not enough charge in battery")
kwhInBattery -= kwhNeeded
}
}
The model in this example is a Car
. It's an EV as you might notice by the kwhInBattery
property. The battery charge is something we might want to visualize in an app. This property has state, or a current value, and this state changes over time. The battery level drains as we drive and (even though it's not implemented in this example) the battery level goes up as it charges. When you visualize this in a UI, you will want to get whatever the current value is, and then be notified of any subsequent changes. In the previous section, I showed you that a PassthroughSubject
can only do the latter; it sends values to its subscribers on-demand without any sense of state or history. A CurrentValueSubject
does have this sense of state. So when you subscribe to a CurrentValueSubject
, you immediately get the current value of that subject, and you are notified when subsequent changes happen. Let's see this in action:
class Car {
var kwhInBattery = CurrentValueSubject<Double, Never>(50.0)
let kwhPerKilometer = 0.14
func drive(kilometers: Double) {
var kwhNeeded = kilometers * kwhPerKilometer
assert(kwhNeeded <= kwhInBattery.value, "Can't make trip, not enough charge in battery")
kwhInBattery.value -= kwhNeeded
}
}
let car = Car()
car.kwhInBattery.sink(receiveValue: { currentKwh in
print("battery has \(currentKwh) remaining")
})
car.drive(kilometers: 200)
First, notice that we create the CurrentValueSubject
with an initial value of 50.0
. This will be the default value that is sent to any new subscribers before the CurrentValueSubject
is mutated. Also notice that inside of the drive(kilometers:)
method, we can get the current value of kwhInBattery
by accessing its value
property. This is something we cannot do with a PassthroughSubject
. When mutating the CurrentValueSubject
, we can change its value and this will automatically cause the publisher to send a new value.
The example above subscribes to the CurrentValueSubject
before we call car.drive(kilometers: 200)
. This means that we will receive a value of 50.0
immediately after subscribing because that is the current value, and we receive a value of 21.999
after driving because the kwhInBattery
value has changed. The CurrentValueSubject
publisher is very useful for stateful, mutable properties like this because we can be sure that we can always get a current value to show in our UI.
Using @Published to publish values
If you've dabbled with SwiftUI a little bit, there's a good chance you've come across the @Published
property wrapper. This property wrapper is a convenient way to create a publisher that behaves a lot like a CurrentValueSubject
with one restriction. You can only mark properties of classes as @Published
. The reason for this is that the @Published
property wrapper needs to create a proxy between the value you're mutating, the object that holds the property and the publisher that is created inside of the property wrapper. This can only work well with reference types because if you'd try this with a struct you would just end up with a bunch of copies that exist on their own rather than pointing to the same object like a reference type does. So, if we have a class, we can often replace CurrentValueSubject
instances with @Published
properties. Let's see this in action on the Car
model I created in the previous section:
class Car {
@Published var kwhInBattery = 50.0
let kwhPerKilometer = 0.14
func drive(kilometers: Double) {
var kwhNeeded = kilometers * kwhPerKilometer
assert(kwhNeeded <= kwhInBattery, "Can't make trip, not enough charge in battery")
kwhInBattery -= kwhNeeded
}
}
let car = Car()
car.$kwhInBattery.sink(receiveValue: { currentKwh in
print("battery has \(currentKwh) remaining")
})
The @Published
property wrapper allows us to access the kwhInBattery
property directly like we normally would. To subscribe to the publisher that is created by the @Published
property wrapper, we need to prefix the wrapped property name with a $
. So in this case car.$kwhInBattery
. When possible, I think it looks slightly nicer to use @Published
over kwhInBattery
because it's easier to access the current value of the publisher.
In Summary
In today's post, I showed you how you can transform existing properties in your code into publishers using CurrentValueSubject
, PassthroughSubject
and even the @Published
property wrapper. You learned the differences, possibilities, and limitations of each publisher. You saw that @Published
is similar to CurrentValueSubject
but it's limited to being used in classes. You also learned that PassthroughSubject
only forwards values with keeping any kind of state while CurrentValueSubject
and @Published
do have a sense of state.
With this knowledge, you should be able to begin using Combine in your projects quite effectively. What's really nice about Combine is that you don't have to integrate it into your project all at once. You can ease your way into using Combine by applying it to small parts of your code first. If you have any questions about this post, or if you have feedback for me, don't hesitate to reach out on Twitter.