在 Swift 3 中使用 KVO 检查值是否被更改
Checking if a value is changed using KVO in Swift 3
我想知道 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()
}
}
然而,编译器报错:"Binary operator '!=' cannot be applied to two 'Any?' operands"
我的第二次尝试:
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()
}
}
}
}
但是,考虑到这一点,我认为这不适用于 swift 对象的原语,例如 Int(我假设)不继承自 NSObject 并且与 Objective-C 不同放入更改字典时,版本不会装箱到 NSNumber 中。
所以,问题是如何完成看似简单的任务来确定是否使用 Swift3 中的 KVO 实际更改了一个值?
此外,奖金问题,我如何使用 'of object' 变量?它不会让我更改名称,当然也不喜欢其中包含空格的变量。
以下是我原来的 Swift 3 答案,但 Swift 4 简化了过程,无需任何转换。例如,如果您正在观察 foo
对象的 Int
属性 称为 bar
:
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.newValue
和 change.oldValue
已正确键入,无需手动转换。如果 属性 是可选的,您可能必须安全地解包它们,但不需要转换。
您唯一需要注意的是确保您的闭包不会引入强引用循环(因此使用 [weak self]
模式)。
下面是我原来的Swift3个回答。
你说:
But, in thinking about this, I don't think this will work for primitives of swift objects such as Int
which (I assume) do not inherit from NSObject
and unlike the Objective-C version won't be boxed into NSNumber
when placed into the change dictionary.
实际上,如果您查看这些值,如果观察到的 属性 是 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()
}
}
或者,如果这是某些 dynamic
属性 或某些 Swift class 的 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()
}
}
你问过:
Also, bonus question, how do I make use of the of object
variable? It won't let me change the name and of course doesn't like variables with spaces in them.
of
是这个参数的外在标签(调用这个方法时用到,这里是OS给我们调用的,所以我们不用这个方法签名中缺少外部标签)。 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
,而不是它的超级 class:
建立的
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()
}
}
我原来的 'second attempt' 包含一个错误,当值更改为 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()
}
}
我想知道 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()
}
}
然而,编译器报错:"Binary operator '!=' cannot be applied to two 'Any?' operands"
我的第二次尝试:
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()
}
}
}
}
但是,考虑到这一点,我认为这不适用于 swift 对象的原语,例如 Int(我假设)不继承自 NSObject 并且与 Objective-C 不同放入更改字典时,版本不会装箱到 NSNumber 中。
所以,问题是如何完成看似简单的任务来确定是否使用 Swift3 中的 KVO 实际更改了一个值?
此外,奖金问题,我如何使用 'of object' 变量?它不会让我更改名称,当然也不喜欢其中包含空格的变量。
以下是我原来的 Swift 3 答案,但 Swift 4 简化了过程,无需任何转换。例如,如果您正在观察 foo
对象的 Int
属性 称为 bar
:
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.newValue
和change.oldValue
已正确键入,无需手动转换。如果 属性 是可选的,您可能必须安全地解包它们,但不需要转换。
您唯一需要注意的是确保您的闭包不会引入强引用循环(因此使用 [weak self]
模式)。
下面是我原来的Swift3个回答。
你说:
But, in thinking about this, I don't think this will work for primitives of swift objects such as
Int
which (I assume) do not inherit fromNSObject
and unlike the Objective-C version won't be boxed intoNSNumber
when placed into the change dictionary.
实际上,如果您查看这些值,如果观察到的 属性 是 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()
}
}
或者,如果这是某些 dynamic
属性 或某些 Swift class 的 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()
}
}
你问过:
Also, bonus question, how do I make use of the
of object
variable? It won't let me change the name and of course doesn't like variables with spaces in them.
of
是这个参数的外在标签(调用这个方法时用到,这里是OS给我们调用的,所以我们不用这个方法签名中缺少外部标签)。 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
,而不是它的超级 class:
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()
}
}
我原来的 'second attempt' 包含一个错误,当值更改为 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()
}
}