Say I have a publisher AnyPublisher<String, Never>, and I wish to assign it to a label's text: String? property. I'm able to do this like so:
class Foo {
var label: UILabel = UILabel()
private var cancellables: Set<AnyCancellable> = []
init(stringPublisher: AnyPublisher<String, Never>) {
stringPublisher
.map { $0 as String? }
.assign(to: \Foo.label.text, on: self)
.store(in: &cancellables)
}
}
Is there a way I can do the same when my label is optional? e.g. because it gets set at a later time? I would like to do something like this:
stringPublisher
.map { $0 as String? }
.assign(to: \Foo.label?.text, on: self)
.store(in: &cancellables)
But I get the error Cannot convert value of type 'KeyPath<Foo, String?>' to expected argument type 'ReferenceWritableKeyPath<Foo, String?>'.
It seems the optional chaining stops the key-path from being writeable. Is there a way to accomplish this, or should I just fall back to using .sink(receiveValue:)?
I think the solution here might depend on when the Foo.label property is set up. If it's only optional because of two-phase initialisation, I'd recommend using a force-unwrap:
stringPublisher.assign(to: \.text, on: label!)
If you can't initialise label early enough, or if Foo.label can refer to different instances, I think you need to use sink(receiveValue:). Not only to workaround the keypath writability issue, but also to fix a memory leak: assign(to:on:) strongly captures its argument, so your code as written creates a strong reference cycle with the self.cancellables collection. I'd go with this code:
I just added an overload for assign that takes a path to an optional value.
/// Assigns a `Publisher`'s `Output` to a property of an object.
///
/// - Note: This is an overload of the existing `assign(to:on:)` which adds an `AnyObject` constraint on `Root` in
/// order to capture the value weakly. Without it, using this method would leak memory. Additionally, it writes to
/// an `Optional` value, preventing type issues between `Optional` and non-`Optional` values.
///
/// - Parameters:
/// - path: `ReferenceWritableKeyPath` indicating the property to assign.
/// - root: Instance that contains the property.
/// - Returns: `AnyCancellable` instance.
func assign<Root: AnyObject>(to path: ReferenceWritableKeyPath<Root, Output?>, on root: Root) -> AnyCancellable {
sink { [weak root] in root?[keyPath: path] = $0 }
}
As a bonus, it avoids the reference cycle as well.