【问题标题】:Checking if a value is changed using KVO in Swift 3在 Swift 3 中使用 KVO 检查值是否已更改
【发布时间】:2017-03-01 18:27:26
【问题描述】:

我想知道 Swift 对象的一组属性何时发生变化。以前,我在 Objective-C 中实现了这一点,但我在将其转换为 Swift 时遇到了一些困难。

我之前的Objective-C代码是:

- (void) observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void*)context {
    if (![change[@"new"] isEqual:change[@"old"]])
        [self edit];
}

我第一次使用 Swift 解决方案是:

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    if change?[.newKey] != change?[.oldKey] {    // Compiler: "Binary operator '!=' cannot be applied to two 'Any?' operands"
        edit()
    }
}

但是,编译器抱怨:“二元运算符 '!=' 不能应用于两个 'Any?'操作数”

我的第二次尝试:

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    if let newValue = change?[.newKey] as? NSObject {
        if let oldValue = change?[.oldKey] as? NSObject {
            if !newValue.isEqual(oldValue) {
                edit()
            }
        }
    }
}

但是,考虑到这一点,我认为这不适用于诸如 Int 之类的 swift 对象的原语,它(我假设)不从 NSObject 继承,并且与 Objective-C 版本不同,它不会被装箱到 NSNumber放入更改字典时。

那么,问题是我该如何做这个看似简单的任务,即在 Swift3 中使用 KVO 确定一个值是否真的被改变了?

另外,还有一个额外的问题,我如何使用“对象”变量?它不会让我更改名称,当然也不喜欢其中有空格的变量。

【问题讨论】:

  • 确保你也看到了这个other answer Rob
  • 请注意,当您使用.initial 时,会有一个 Swift4 错误。有关解决方案,请参阅here

标签: swift swift3 key-value-observing


【解决方案1】:

以下是我最初的 Swift 3 答案,但 Swift 4 简化了流程,无需任何转换。例如,如果您正在观察 Int 对象的 bar 属性 foo

class Foo: NSObject {
    @objc dynamic var bar: Int = 42
}

class ViewController: UIViewController {

    let foo = Foo()
    var token: NSKeyValueObservation?

    override func viewDidLoad() {
        super.viewDidLoad()

        token = foo.observe(\.bar, options: [.new, .old]) { [weak self] object, change in
            if change.oldValue != change.newValue {
                self?.edit()
            }
        }
    }

    func edit() { ... }
}

注意,这种基于闭包的方法:

  • 让您无需实现单独的observeValue 方法;

  • 无需指定context 并检查该上下文;和

  • change.newValuechange.oldValue 键入正确,无需手动转换。如果该属性是可选的,您可能必须安全地展开它们,但不需要强制转换。

您唯一需要注意的是确保您的闭包不会引入强引用循环(因此使用[weak self] 模式)。


我原来的 Swift 3 答案如下。


你说:

但是,考虑到这一点,我认为这不适用于 swift 对象的原语,例如 Int,它(我假设)不会从 NSObject 继承,并且与 Objective-C 版本不同的是不会放入更改字典时,被装箱到NSNumber

实际上,如果您查看这些值,如果观察到的属性是 Int,它确实会以 NSNumber 的形式出现在字典中。

所以,您可以留在NSObject 世界中:

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    if let newValue = change?[.newKey] as? NSObject,
        let oldValue = change?[.oldKey] as? NSObject,
        !newValue.isEqual(oldValue) {
            edit()
    }
}

或将它们用作NSNumber

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    if let newValue = change?[.newKey] as? NSNumber,
        let oldValue = change?[.oldKey] as? NSNumber,
        newValue.intValue != oldValue.intValue {
            edit()
    }
}

或者,如果这是某个 Swift 类的某个 dynamic 属性的 Int 值,我会继续将它们转换为 Int

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    if let newValue = change?[.newKey] as? Int, let oldValue = change?[.oldKey] as? Int, newValue != oldValue {
        edit()
    }
}

你问:

另外,额外的问题,我如何使用of object 变量?它不会让我更改名称,当然也不喜欢其中有空格的变量。

of 是此参数的外部标签(在您调用此方法时使用;在这种情况下,操作系统会为我们调用此方法,因此我们不会在方法签名中使用此外部标签)。 object 是内部标签(在方法本身中使用)。 Swift 拥有为参数设置外部和内部标签的能力已经有一段时间了,但它只是在 Swift 3 的 API 中才真正被接受。

就您何时使用此change 参数而言,如果您要观察多个对象的属性,并且如果这些对象需要在KVO 上进行不同处理,则使用它,例如:

foo.addObserver(self, forKeyPath: #keyPath(Foo.bar), options: [.new, .old], context: &observerContext)
baz.addObserver(self, forKeyPath: #keyPath(Foo.qux), options: [.new, .old], context: &observerContext)

然后:

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    guard context == &observerContext else {
        super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
        return
    }

    if (object as? Foo) == foo {
        // handle `foo` related notifications here
    }
    if (object as? Baz) == baz {
        // handle `baz` related notifications here
    }
}

顺便说一句,我通常建议使用context,例如,使用private var

private var observerContext = 0

然后使用该上下文添加观察者:

foo.addObserver(self, forKeyPath: #keyPath(Foo.bar), options: [.new, .old], context: &observerContext)

然后让我的observeValue 确保它是它的context,而不是它的超类建立的:

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    guard context == &observerContext else {
        super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
        return
    }

    if let newValue = change?[.newKey] as? Int, let oldValue = change?[.oldKey] as? Int, newValue != oldValue {
        edit()
    }
}

【讨论】:

  • 有趣,尽管值可以是任何值(不仅仅是 Int,即字符串、日期、枚举或自定义类等)。所以,也许 NSObject 技巧会起作用。谢谢你的回答。
  • 是的,NSObject 技术很好。我只是向你展示了选项。顺便说一句,您只能使用可以在 Objective-C 中表示的类型进行 KVO(例如,您无法观察 enum;您必须拥有带有原始值的 enum,并且只存储 @987654361 @,而不是 enum,本身)。
  • 哦...我理解了ofObject参数的概念,但是objective-c运算符ofObject被转换为'of object'。显然,swift会忽略属性名称前的一个单词,以便开发人员为参数提供上下文?
  • 哦,我以为你问的是如何使用这个参数。当您看到of object 时,of 是外部参数标签(此处并不适用,因为操作系统调用它,而不是您),object 是内部标签(您在您在此方法中的代码)。这种使用内部和外部参数名称的约定已经成为 Swift 的一部分,但 Apple 终于在他们对 Swift 3 的 API 重新设计中真正接受了它。请参阅 Argument labels
  • 我正在阅读here。试图找出当我想捕获 initial 值时应该使用什么键。看来我应该做类似if let change = change?[.oldKey] 的事情。那正确吗?这意味着如果添加像options: [.new,.initial] 这样的观察者...我可以使用.newKey.oldKey 捕获它们
【解决方案2】:

我最初的“第二次尝试”包含一个错误,当值更改为 nil 或从 nil 更改时,该错误将无法检测到。一旦意识到'change'的所有值都是NSObject,第一次尝试实际上可以修复:

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    let oldValue: NSObject? = change?[.oldKey] as? NSObject
    let newValue: NSObject? = change?[.newKey] as? NSObject
    if newValue != oldValue {
        edit()
    }
}

如果您愿意,也可以使用更简洁的版本:

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    if change?[.newKey] as? NSObject != change?[.oldKey] as? NSObject {
        edit()
    }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-02-20
    • 1970-01-01
    • 1970-01-01
    • 2018-01-08
    • 2013-08-11
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多