swift - Swift ReferenceWritableKeyPath 如何使用 Optional 属性?

标签 swift optional combine swift-keypath

存在地 :在阅读之前,了解您不能将 UIImage 分配给 ImageView socket 的 image 会有所帮助。属性通过 key 路径 \UIImageView.image .这是属性(property):

@IBOutlet weak var iv: UIImageView!

现在,这会编译吗?
    let im = UIImage()
    let kp = \UIImageView.image
    self.iv[keyPath:kp] = im // error

不!

Value of optional type 'UIImage?' must be unwrapped to a value of type 'UIImage'



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

我真正想了解的是Combine 框架如何.assign订阅者在幕后工作。为了进行实验,我尝试使用我自己的 Assign 对象。在我的示例中,我的发布者管道生成一个 UIImage 对象,并将其分配给 image UIImageView 属性的属性 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是一个 optional UIImage,我的发布者生成了一个简单明了的 UIImage。

我试图通过在关键路径中解开 Optional 来解决这个问题:
let assign = Subscribers.Assign(object: self.iv, keyPath: \UIImageView.image!)
pub.subscribe(assign)

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

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

在 Martin R 的一些输入之后,我意识到如果我们输入 pub明确作为生产 UIImage?我们得到与添加 map 相同的效果将 UIImage 包装在 Optional 中。所以这编译和工作
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是怎么回事作品。似乎可以将类型的 optional 性插入管道中 .receive运营商。但我不明白这怎么可能。

最佳答案

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

  • Swift 一次推断整个语句的类型,而不是跨语句。
  • Swift 允许类型推断自动提升类型 T 的对象输入 Optional<T> , 如有必要进行语句类型检查。
  • Swift 还允许类型推断自动提升类型 (A) -> B 的闭包输入 (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.imageValue类型是 UIImage? , 和 self.iv的类型是 UIImageView! , Swift 必须做两件“自动”的事情来让这个语句进行类型检查:
  • 它有促进关闭{ UIImage(data: $0) }来自类型 (Data) -> UIImage?输入 (Data) -> UIImage??以便 compactMap可以剥离一层Optional并制作 Output类型是 UIImage? .
  • 它必须隐式解包 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 进行类型检查陈述。它必须隐式解包 iv ,但不需要任何 Optional晋升。它推导出 Input输入为 UIImage?因为那是 Value key 路径的类型。

    最后,Swift 尝试对 subscribe 进行类型检查。陈述。 Output p1 的类型是 UIImage ,以及 Input a1 的类型是 UIImage? .它们是不同的,因此 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因为否则 p1的类型太难看,无法拼出。

    Subscribers.Assign.init是公开的,我们也可以直接使用它让 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返回这里。

    现在,回到基于键路径的分配:
    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)
        }
    }
    

    如果我们更改 iv 的类型,它也会编译至 UIImageView?并选择分配:
    class Thing {
        var iv: UIImageView? = UIImageView()
    
        func test() {
            let im = UIImage()
            let kp = \UIImageView.image
            self.iv?[keyPath: kp] = im
        }
    }
    

    但是如果我们只是强制解包隐式解包的 optional 项,它就不会编译:
    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
        }
    }
    

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

    关于swift - Swift ReferenceWritableKeyPath 如何使用 Optional 属性?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59637100/

    相关文章:

    iOS 委托(delegate)而不是通过 segue 传递数据

    c++ - std::optional:不参与重载决议与被定义为已删除

    java - 如何以函数式风格多次写入 "if optional is empty, call next method returning optional, if not return this non-empty optional"?

    core-data - 当 SwiftUI 中的相关实体发生更改时,如何更新 @FetchRequest?

    ios - map 注释不会显示 rightCalloutAccessoryView 的自定义 UIButton

    ios - 通过解析与 iOS 推送通知进行深度链接

    scala - 在 Scala 中转换多个 optional 值

    SwiftUI @Binding 值无法更改并调用 init

    ios - 当最小部署目标设置为 iOS 12.2 时,如何有条件地使用 Combine @Published?

    ios - 在 swift 警报中使用哪个标识符代替 "present"