Updating UI with assign(to:on:) in Combine
Published on: January 29, 2020So far in my series of posts about Combine, we have focussed on processing values and publishing them. In all of these posts, I used the sink
method to subscribe to publishers and to handle their results. Today I would like to show you a different kind of built-in subscriber; assign(to:on:)
. This subscriber is perfect for subscribing to publishers and updating your UI in response to new values. In this post, I will show you how to use this subscriber, and I will show you how to avoid memory issues when using assign(to:on:)
.
Using assign(to:on:) in your code
If you've been following along with previous posts, you should at least have a basic understanding of publishers and how you subscribe to them. If you haven't and aren't sure how publishers and subscribers work, I would recommend reading the following posts before coming back to this one:
If you subscribe to a publisher using sink, and you want to update your UI in response to changes to a certain property you might write something like the following code:
class CarViewController: UIViewController {
let car = Car()
let label = UILabel()
var cancellables = Set<AnyCancellable>()
func subscribeToCarCharge() {
car.$kwhInBattery.sink(receiveValue: { charge in
self.label.text = "Car's charge is \(charge)"
}).store(in: &cancellables)
}
// more VC code...
}
This code should look familiar if you've read my post about publishing property changes in Combine. If you haven't, all you really need to know is that car.$kwhInBattery
is a publisher that publishes a double value that represents a car's battery charge.
Note that we're using AnyCancellable
's store(in:)
method to retain the AnyCancellable
that is returned by sink
to avoid it from getting deallocated and tearing down the subscription as soon as as subscribeToCarCharge
finishes executing.
All in all the above code should look familiar and it's quite effective. But what if I told you that there was a slightly nicer way to do this using assign(to:on:)
instead of sink
:
func subscribeToCarCharge() {
car.$kwhInBattery
.map { "Car's charge is \($0)" }
.assign(to: \.text, on: label)
.store(in: &cancellables)
}
The preceding code isn't much shorter, but it definitely is more declarative. It communicates that we want to map
the Double
that is provided by $kwhInBattery
into a String
, and that we want to assign that string to the text
property on label
. The assign(to:on:)
method returns an AnyCancellable
, just like sink
. So we need to retain it to make sure it doesn't get deallocated.
Using assign(to:on:)
becomes very interesting if you use an architecture where your model prepares data for your view in a way where no further processing is required, like MVVM:
struct CarViewModel {
private let car = Car()
let chargeRemainingText: AnyPublisher<String?, Never>
init() {
chargeRemainingText = car.$kwhInBattery.map {
"Car's charge is \($0)"
}.eraseToAnyPublisher()
}
}
class CarViewController: UIViewController {
let viewModel = CarViewModel()
let label = UILabel()
var cancellables = Set<AnyCancellable>()
func subscribeToCarCharge() {
viewModel.chargeRemainingText
.assign(to: \.text, on: label)
.store(in: &cancellables)
}
// More VC code...
}
In the preceding example, all mapping is done by the ViewModel
and the label
can be subscribed to the chargeRemainingText
publisher directly. Note that we need to convert chargeRemainingText
to AnyPublisher
because it's type would be Publishers.Map<Published<Double>.Publisher, String>
if we didn't. With all of the above examples, you should now be able to begin using assign(to:on:)
where it makes sense.
Avoiding retains cycles when using assign(to:on:)
While I was experimenting with assign(to:on:)
I found out that there are cases where it might cause retain cycles. Here's an example where that might occur:
var subscription: AnyCancellable?
func subscribeToCarCharge() {
subscription = viewModel.chargeRemainingText
.assign(to: \.label.text, on: self)
}
The code above uses self
as the target for the assignment, while self
also holds on to the AnyCancellable
that is returned by assign(to:on:)
. At this time there isn't much you can do other than implementing a workaround, or avoiding assignment to self
. I personally hope this is a bug in Combine and that a future release will fix this leak.
In summary
Today you learned about Combine's assign(to:on:)
subscriber. You saw that it's a special kind of subscriber that allows you to easily take values that are published by a publisher, and assign them to a property on one of your UI elements, or any other property on any other object for that matter.
You also saw that there are some considerations to keep in mind when using assign(to:on:)
, for example when you use it to assign a property on self
.
If you have any questions about this post, or if you have feedback for me. Please reach out on Twitter.