Publishing property changes in Combine

Published on: January 27, 2020

In 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.

Categories

Combine

Subscribe to my newsletter