Updating UI with assign(to:on:) in Combine

Published on: January 29, 2020

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

Categories

Combine Quick Tip

Subscribe to my newsletter