13

存在的基础:在阅读之前,了解您不能通过imagekeypath 将 UIImage 分配给图像视图插座的属性会有所帮助\UIImageView.image。这是财产:

@IBOutlet weak var iv: UIImageView!

现在,这会编译吗?

    let im = UIImage()
    let kp = \UIImageView.image
    self.iv[keyPath:kp] = im // error

不!

可选类型“UIImage?”的值 必须解包为“UIImage”类型的值

好的,现在我们已经为实际用例做好了准备。


我实际上想了解的是 Combine 框架.assign订阅者如何在幕后工作。为了进行实验,我尝试使用自己的 Assign 对象。在我的示例中,我的发布者管道生成一个 UIImage 对象,并将其分配给imageUIImageView property 的属性self.iv

如果我们使用该.assign方法,它将编译并工作:

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)

所以,我对自己说,要看看它是如何工作的,我将删除.assign并用我自己的 Assign 对象替换它:

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)

哗啦啦!我们不能这样做,因为UIImageView.image它是一个可选的 UIImage,而我的发布者生成了一个简单明了的 UIImage。

我试图通过在键路径中展开 Optional 来解决这个问题:

let assign = Subscribers.Assign(object: self.iv, keyPath: \UIImageView.image!)
pub.subscribe(assign)

酷,编译。但它在运行时崩溃,大概是因为图像视图的图像最初是nil.

现在我可以通过map在我的管道中添加一个将 UIImage 包装在 Optional 中的管道来解决所有这些问题,以便所有类型都正确匹配。但我的问题是,这到底是如何工作的?我的意思是,为什么我不必在我使用的第一个代码中这样做.assign?为什么我可以在.image那里指定密钥路径?关于关键路径如何与可选属性一起工作似乎有些技巧,但我不知道它是什么。


在 Martin RI 的一些输入之后意识到,如果我们pub显式键入作为生成,我们将获得与添加将 UIImage 包装在 Optional 中的UIImage?效果相同的效果。map所以这编译和工作

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)

这仍然不能解释原件是如何.assign工作的。似乎它能够将类型可选性通过管道推送到运算符中。但我不明白这怎么可能。.receive

4

1 回答 1

9

您(马特)可能至少已经知道其中的一些,但这里有一些事实供其他读者参考:

  • Swift 一次推断一个完整语句的类型,而不是跨语句推断类型。

  • Swift 允许类型推断自动将类型对象提升T为 type Optional<T>,如果需要对语句进行类型检查。

  • Swift 还允许类型推断自动将 type 的闭包提升(A) -> B为 type (A) -> B?。换句话说,这编译:

    let a: (Data) -> UIImage? = { UIImage(data: $0) }
    let b: (Data) -> UIImage?? = a
    

    这让我很意外。我在调查您的问题时发现了它。

现在让我们考虑使用assign

let p0 = Just(Data())
    .compactMap { UIImage(data: $0) }
    .receive(on: DispatchQueue.main)
    .assign(to: \.image, on: self.iv)

Swift 同时对整个语句进行类型检查。由于\UIImageView.image's Valuetype isUIImage?self.iv's type is UIImageView!, Swift 必须做两件“自动”的事情来使这个语句类型检查:

  • 它必须促进{ UIImage(data: $0) }从类型(Data) -> UIImage?到类型的闭包,(Data) -> UIImage??以便compactMap可以剥离一层Optional并使Output类型成为UIImage?

  • 它必须隐式地 unwrap iv,因为Optional<UIImage>没有名为的属性image,但UIImage确实如此。

这两个动作让 Swift 成功地对语句进行类型检查。

现在假设我们将其分解为三个语句:

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 首先对let p1语句进行类型检查。它不需要提升闭包类型,所以它可以推导出Output一个UIImage.

然后 Swift 对该let a1语句进行类型检查。它必须隐式地 unwrap iv,但不需要任何Optional促销。它将类型推断InputUIImage?因为这是Value关键路径的类型。

最后,Swift 尝试对subscribe语句进行类型检查。的Output类型p1UIImage,的Input类型a1UIImage?。它们是不同的,因此 Swift 无法成功地对语句进行类型检查。Swift 不支持Optional推广泛型类型参数,如InputOutput。所以这不会编译。

我们可以通过强制Output类型为p1来进行这种类型检查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)

在这里,我们强制 Swift 推广闭包类型。我用了eraseToAnyPublisher因为 elsep1的类型太难写了。

由于Subscribers.Assign.init是 public,我们也可以直接使用它来让 Swift 推断所有类型:

let p2 = Just(Data())
    .compactMap { UIImage(data: $0) }
    .receive(on: DispatchQueue.main)
    .subscribe(Subscribers.Assign(object: self.iv, keyPath: \.image))

Swift 类型检查成功。它与前面使用的语句基本相同.assign。请注意,它推断类型()为,p2因为这就是.subscribe这里返回的内容。


现在,回到基于 keypath 的分配:

class Thing {
    var iv: UIImageView! = UIImageView()

    func test() {
        let im = UIImage()
        let kp = \UIImageView.image
        self.iv[keyPath: kp] = im
    }
}

这不编译,错误value of optional type 'UIImage?' must be unwrapped to a value of type 'UIImage'。我不知道为什么 Swift 不能编译这个。如果我们显式转换im为,它会编译UIImage?

class Thing {
    var iv: UIImageView! = UIImageView()

    func test() {
        let im = UIImage()
        let kp = \UIImageView.image
        self.iv[keyPath: kp] = .some(im)
    }
}

如果我们更改ivto的类型UIImageView?并可选化赋值,它也会编译:

class Thing {
    var iv: UIImageView? = UIImageView()

    func test() {
        let im = UIImage()
        let kp = \UIImageView.image
        self.iv?[keyPath: kp] = im
    }
}

但是如果我们只是强制解包隐式解包的可选项,它不会编译:

class Thing {
    var iv: UIImageView! = UIImageView()

    func test() {
        let im = UIImage()
        let kp = \UIImageView.image
        self.iv![keyPath: kp] = im
    }
}

如果我们只是选择赋值,它不会编译:

class Thing {
    var iv: UIImageView! = UIImageView()

    func test() {
        let im = UIImage()
        let kp = \UIImageView.image
        self.iv?[keyPath: kp] = im
    }
}

我认为这可能是编译器中的错误。

于 2020-01-11T06:05:13.297 回答