Ground of Being: It will help, before reading, to know that you cannot assign a UIImage to an image view outlet's image
property through the keypath \UIImageView.image
. Here's the property:
@IBOutlet weak var iv: UIImageView!
Now, will this compile?
let im = UIImage()
let kp = \UIImageView.image
self.iv[keyPath:kp] = im // error
No!
Value of optional type 'UIImage?' must be unwrapped to a value of type 'UIImage'
Okay, now we're ready for the actual use case.
What I'm actually trying to understand is how the Combine framework .assign
subscriber works behind the scenes. To experiment, I tried using my own Assign object. In my example, my publisher pipeline produces a UIImage object, and I assign it to the image
property of a UIImageView property self.iv
.
If we use the .assign
method, this compiles and works:
URLSession.shared.dataTaskPublisher(for: url)
.map {$0.data}
.replaceError(with: Data())
.compactMap { UIImage(data:$0) }
.receive(on: DispatchQueue.main)
.assign(to: \.image, on: self.iv)
.store(in:&self.storage)
So, says I to myself, to see how this works, I'll remove the .assign
and replace it with my own Assign object:
let pub = URLSession.shared.dataTaskPublisher(for: url)
.map {$0.data}
.replaceError(with: Data())
.compactMap { UIImage(data:$0) }
.receive(on: DispatchQueue.main)
let assign = Subscribers.Assign(object: self.iv, keyPath: \UIImageView.image)
pub.subscribe(assign) // error
// (and we will then wrap in AnyCancellable and store)
Blap! We can't do that, because UIImageView.image
is an Optional UIImage, and my publisher produces a UIImage plain and simple.
I tried to work around this by unwrapping the Optional in the key path:
let assign = Subscribers.Assign(object: self.iv, keyPath: \UIImageView.image!)
pub.subscribe(assign)
Cool, that compiles. But it crashes at runtime, presumably because the image view's image is initially nil
.
Now I can work around all of this just fine by adding a map
to my pipeline that wraps the UIImage up in an Optional, so that all the types match correctly. But my question is, how does this really work? I mean, why don't I have to do that in the first code where I use .assign
? Why am I able to specify the .image
keypath there? There seems to be some trickery about how key paths work with Optional properties but I don't know what it is.
After some input from Martin R I realized that if we type pub
explicitly as producing UIImage?
we get the same effect as adding a map
that wraps the UIImage in an Optional. So this compiles and works
let pub : AnyPublisher<UIImage?,Never> = URLSession.shared.dataTaskPublisher(for: url)
.map {$0.data}
.replaceError(with: Data())
.compactMap { UIImage(data:$0) }
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
let assign = Subscribers.Assign(object: self.iv, keyPath: \UIImageView.image)
pub.subscribe(assign)
let any = AnyCancellable(assign)
any.store(in:&self.storage)
This still doesn't explain how the original .assign
works. It appears that it is able to push the optionality of the type up the pipeline into the .receive
operator. But I don't see how that is possible.