You (Matt) probably know at least some of this already, but here are some facts for other readers:
-
Swift infers types on one whole statement at a time, but not across statements.
-
Swift allows type inference to automatically promote an object of type
T
to typeOptional<T>
, if necessary to make the statement type-check. -
Swift also allows type inference to automatically promote a closure of type
(A) -> B
to type(A) -> B?
. In other words, this compiles:let a: (Data) -> UIImage? = { UIImage(data: $0) } let b: (Data) -> UIImage?? = a
This came as a surprise to me. I discovered it while investigating your problem.
Now let’s consider the use of assign
:
let p0 = Just(Data())
.compactMap { UIImage(data: $0) }
.receive(on: DispatchQueue.main)
.assign(to: \.image, on: self.iv)
Swift type-checks this entire statement simultaneously. Since \UIImageView.image
‘s Value
type is UIImage?
, and self.iv
‘s type is UIImageView!
, Swift has to do two “automatic” things to make this statement type-check:
-
It has to promote the closure
{ UIImage(data: $0) }
from type(Data) -> UIImage?
to type(Data) -> UIImage??
so thatcompactMap
can strip off one level ofOptional
and make theOutput
type beUIImage?
. -
It has to implicitly unwrap
iv
, becauseOptional<UIImageView>
has no property namedimage
, butUIImageView
does.
These two actions let Swift type-check the statement successfully.
Now suppose we break it into three statements:
let p1 = Just(Data())
.compactMap { UIImage(data: $0) }
.receive(on: DispatchQueue.main)
let a1 = Subscribers.Assign(object: self.iv, keyPath: \.image)
p1.subscribe(a1)
Swift first type-checks the let p1
statement. It has no need to promote the closure type, so it can deduce an Output
type of UIImage
.
Then Swift type-checks the let a1
statement. It must implicitly unwrap iv
, but there’s no need for any Optional
promotion. It deduces the Input
type as UIImage?
because that is the Value
type of the key path.
Finally, Swift tries to type-check the subscribe
statement. The Output
type of p1
is UIImage
, and the Input
type of a1
is UIImage?
. These are different, so Swift cannot type-check the statement successfully. Swift does not support Optional
promotion of generic type parameters like Input
and Output
. So this doesn’t compile.
We can make this type-check by forcing the Output
type of p1
to be UIImage?
:
let p1: AnyPublisher<UIImage?, Never> = Just(Data())
.compactMap { UIImage(data: $0) }
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
let a1 = Subscribers.Assign(object: self.iv, keyPath: \.image)
p1.subscribe(a1)
Here, we force Swift to promote the closure type. I used eraseToAnyPublisher
because otherwise p1
‘s type is too ugly to spell out.
Since Subscribers.Assign.init
is public, we can also use it directly to make Swift infer all the types:
let p2 = Just(Data())
.compactMap { UIImage(data: $0) }
.receive(on: DispatchQueue.main)
.subscribe(Subscribers.Assign(object: self.iv, keyPath: \.image))
Swift type-checks this successfully. It is essentially the same as the statement that used .assign
earlier. Note that it infers type ()
for p2
because that’s what .subscribe
returns here.
Now, back to your keypath-based assignment:
class Thing {
var iv: UIImageView! = UIImageView()
func test() {
let im = UIImage()
let kp = \UIImageView.image
self.iv[keyPath: kp] = im
}
}
This doesn’t compile, with the error value of optional type 'UIImage?' must be unwrapped to a value of type 'UIImage'
. I don’t know why Swift can’t compile this. It compiles if we explicitly convert im
to UIImage?
:
class Thing {
var iv: UIImageView! = UIImageView()
func test() {
let im = UIImage()
let kp = \UIImageView.image
self.iv[keyPath: kp] = .some(im)
}
}
It also compiles if we change the type of iv
to UIImageView?
and optionalize the assignment:
class Thing {
var iv: UIImageView? = UIImageView()
func test() {
let im = UIImage()
let kp = \UIImageView.image
self.iv?[keyPath: kp] = im
}
}
But it does not compile if we just force-unwrap the implicitly-unwrapped optional:
class Thing {
var iv: UIImageView! = UIImageView()
func test() {
let im = UIImage()
let kp = \UIImageView.image
self.iv![keyPath: kp] = im
}
}
And it does not compile if we just optionalize the assignment:
class Thing {
var iv: UIImageView! = UIImageView()
func test() {
let im = UIImage()
let kp = \UIImageView.image
self.iv?[keyPath: kp] = im
}
}
I think this might be a bug in the compiler.