键值观察(KVO)在 Swift 中是否可用?

如果是这样,那么在 Objective-C 中使用关键值观察时,是否存在其他方面不存在的关键差异?

115528 次浏览

(编辑添加新信息) : 考虑是否使用组合框架可以帮助您完成您想要的,而不是使用 KVO

是也不是。KVO 一如既往地在 NSObject 子类上工作。它不适用于没有子类 NSObject 的类。斯威夫特没有(至少目前没有)自己的天然观测系统。

(有关如何将其他属性作为 ObjecC 公开以便 KVO 处理它们的问题,请参阅注释)

有关完整示例,请参见 苹果文档

目前 Swift 不支持任何内置的机制来观察对象的属性变化,除了“自我”,所以不,它不支持 KVO。

然而,KVO 是 Objective-C 和 Cocoa 的一个非常基本的组成部分,因此将来很可能会添加它。目前的文件似乎暗示了这一点:

键值观察

信息马上就到。

与 Cocoa 和 Objective-C 一起使用 Swift

举个例子可能会有所帮助。如果我有一个具有 namestate属性的类 Model的实例 model,我可以用以下方法观察这些属性:

let options = NSKeyValueObservingOptions([.New, .Old, .Initial, .Prior])


model.addObserver(self, forKeyPath: "name", options: options, context: nil)
model.addObserver(self, forKeyPath: "state", options: options, context: nil)

对这些属性的更改将触发对以下内容的调用:

override func observeValueForKeyPath(keyPath: String!,
ofObject object: AnyObject!,
change: NSDictionary!,
context: CMutableVoidPointer) {


println("CHANGE OBSERVED: \(change)")
}

答案是肯定的,也是否定的:

  • Yes ,您可以使用 Swift 中相同的 KVO API 来观察 Objective-C 对象。
    还可以观察从 NSObject继承的 Swift 对象的 dynamic属性。
    但是... 没有并不像你想象的那样是一个强类型的 Swift 本地观测系统。
    使用 Swift 与 Cocoa 和 Objective-C | 关键值观察

  • 没有 ,目前没有针对任意 Swift 对象的内置值观测系统。

  • 是的 ,有内建的 物业观察员,它是强类型的。
    但是... ... 没有它们不是 KVO,因为它们只允许观察对象自己的属性,不支持嵌套观察(“关键路径”) ,并且您必须显式地实现它们。
    快速编程语言 | 属性观察员

  • ,您可以实现显式的值观察,这将是强类型的,并允许从其他对象添加多个处理程序,甚至支持嵌套/“键路径”。
    但是... 没有它将不会是 KVO,因为它将只工作的属性,你实施作为观察。
    您可以在这里找到实现这种观察值的库:
    用于快速值观测和事件的可观测快速 KVO

是的。

KVO 需要克劳斯·福尔曼,所以你只需要在方法、属性、下标或初始化器中添加 dynamic修饰符:

dynamic var foo = 0

dynamic修饰符确保对声明的引用将通过 objc_msgSend动态分派和访问。

您可以在 Swift 中使用 KVO,但只能用于 NSObject子类的 dynamic属性。假设您希望观察 Foo类的 bar属性。在 Swift 4中,将 bar指定为 NSObject子类中的 dynamic属性:

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

然后可以注册以观察对 bar属性的更改。在 Swift 4和 Swift 3.2中,这已经被大大简化了,如 关键值观测在 Swift 中的应用所概述的:

class MyObject {
private var token: NSKeyValueObservation


var objectToObserve = Foo()


init() {
token = objectToObserve.observe(\.bar) { [weak self] object, change in  // the `[weak self]` is to avoid strong reference cycle; obviously, if you don't reference `self` in the closure, then `[weak self]` is not needed
print("bar property is now \(object.bar)")
}
}
}

注意,在 Swift 4中,我们现在使用反斜杠字符强键入键路径(\.bar是所观察对象的 bar属性的键路径)。另外,因为它使用了完成闭包模式,我们不必手动删除观察者(当 token超出作用域时,观察者就被删除了) ,也不必担心如果键不匹配就调用 super实现。仅当调用这个特定的观察者时才调用闭包。有关更多信息,请参见 WWDC 2017视频,基金会最新消息

在 Swift 3中,要观察到这一点,需要稍微复杂一些,但与 Objective-C 中的操作非常相似。也就是说,您将实现 observeValue(forKeyPath keyPath:, of object:, change:, context:),它(a)确保我们正在处理我们的上下文(而不是我们的 super实例已经注册要观察的内容) ; 然后(b)处理它或者在必要时将其传递给 super实现。并确保在适当的时候不要以旁观者的身份出现。例如,您可以在释放观察器时删除它:

雨燕3:

class MyObject: NSObject {
private var observerContext = 0


var objectToObserve = Foo()


override init() {
super.init()


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


deinit {
objectToObserve.removeObserver(self, forKeyPath: #keyPath(Foo.bar), 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
}


// do something upon notification of the observed object


print("\(keyPath): \(change?[.newKey])")
}


}

注意,您只能观察可以在 Objective-C 中表示的属性。因此,您不能观察泛型、 Swift struct类型、 Swift enum类型等。

有关 Swift 2实现的讨论,请参见下面我的原始答案。


使用 dynamic关键字实现具有 NSObject子类的 KVO 在 与 Cocoa 和 Objective-C 一起使用 Swift指南的 采用可可设计规范章的 键值观察节中有描述:

键值观察是一种机制,它允许将其他对象指定属性的更改通知对象。您可以对 Swift 类使用键值观察,只要该类继承自 NSObject类。您可以使用这三个步骤在 Swift 中实现键值观察。

  1. dynamic修饰符添加到您想要观察的任何属性中。有关 dynamic的更多信息,请参见 需要克劳斯·福尔曼

    class MyObjectToObserve: NSObject {
    dynamic var myDate = NSDate()
    func updateDate() {
    myDate = NSDate()
    }
    }
    
  2. Create a global context variable.

    private var myContext = 0
    
  3. Add an observer for the key-path, and override the observeValueForKeyPath:ofObject:change:context: method, and remove the observer in deinit.

    class MyObserver: NSObject {
    var objectToObserve = MyObjectToObserve()
    override init() {
    super.init()
    objectToObserve.addObserver(self, forKeyPath: "myDate", options: .New, context: &myContext)
    }
    
    
    override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
    if context == &myContext {
    if let newValue = change?[NSKeyValueChangeNewKey] {
    print("Date changed: \(newValue)")
    }
    } else {
    super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
    }
    }
    
    
    deinit {
    objectToObserve.removeObserver(self, forKeyPath: "myDate", context: &myContext)
    }
    }
    

[Note, this KVO discussion has subsequently been removed from the Using Swift with Cocoa and Objective-C guide, which has been adapted for Swift 3, but it still works as outlined at the top of this answer.]


It's worth noting that Swift has its own native property observer system, but that's for a class specifying its own code that will be performed upon observation of its own properties. KVO, on the other hand, is designed to register to observe changes to some dynamic property of some other class.

值得一提的是,在更新你的 Xcode之后,你可能会得到以下信息: “方法不重写其超类中的任何方法” 。这是因为论点的可选性。请确保您的观察处理程序看起来完全如下:

override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [NSObject : AnyObject]?, context: UnsafeMutablePointer<Void>)

这个类必须从 NSObject继承,我们有3种方法来触发属性的改变

NSKeyValueCoding使用 setValue(value: AnyObject?, forKey key: String)

class MyObjectToObserve: NSObject {
var myDate = NSDate()
func updateDate() {
setValue(NSDate(), forKey: "myDate")
}
}

使用 NSKeyValueObserving中的 willChangeValueForKeydidChangeValueForKey

class MyObjectToObserve: NSObject {
var myDate = NSDate()
func updateDate() {
willChangeValueForKey("myDate")
myDate = NSDate()
didChangeValueForKey("myDate")
}
}

使用 dynamic。参见 快速类型兼容性

如果您正在使用诸如键值观察这样的 API 来动态地替换方法的实现,那么您还可以使用动态修饰符来要求通过 Objective-C 运行时动态地分派对成员的访问。

class MyObjectToObserve: NSObject {
dynamic var myDate = NSDate()
func updateDate() {
myDate = NSDate()
}
}

属性 getter 和 setter 在使用时调用。您可以在使用 KVO 时进行验证。这是一个计算属性的示例

class MyObjectToObserve: NSObject {
var backing: NSDate = NSDate()
dynamic var myDate: NSDate {
set {
print("setter is called")
backing = newValue
}
get {
print("getter is called")
return backing
}
}
}

对于遇到类型(如 Int)问题的人来说,这是另一个例子吗?和 CGFloat?.您只需将您的类设置为 NSObject 的子类并声明您的变量,例如:

class Theme : NSObject{


dynamic var min_images : Int = 0
dynamic var moreTextSize : CGFloat = 0.0


func myMethod(){
self.setValue(value, forKey: "\(min_images)")
}


}

这可能证明有助于少数人-

// MARK: - KVO


var observedPaths: [String] = []


func observeKVO(keyPath: String) {
observedPaths.append(keyPath)
addObserver(self, forKeyPath: keyPath, options: [.old, .new], context: nil)
}


func unObserveKVO(keyPath: String) {
if let index = observedPaths.index(of: keyPath) {
observedPaths.remove(at: index)
}
removeObserver(self, forKeyPath: keyPath)
}


func unObserveAllKVO() {
for keyPath in observedPaths {
removeObserver(self, forKeyPath: keyPath)
}
}


override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if let keyPath = keyPath {
switch keyPath {
case #keyPath(camera.iso):
slider.value = camera.iso
default:
break
}
}
}

我已经在 Swift 3中以这种方式使用了 KVO。

概述

使用 Combine而不使用 NSObjectObjective-C是可能的

可用性: iOS 13.0+macOS 10.15+tvOS 13.0+watchOS 6.0+Mac Catalyst 13.0+Xcode 11.0+

注意: 需要仅用于没有值类型的类。

密码:

迅速版本: 5.1.2

import Combine //Combine Framework


//Needs to be a class doesn't work with struct and other value types
class Car {


@Published var price : Int = 10
}


let car = Car()


//Option 1: Automatically Subscribes to the publisher


let cancellable1 = car.$price.sink {
print("Option 1: value changed to \($0)")
}


//Option 2: Manually Subscribe to the publisher
//Using this option multiple subscribers can subscribe to the same publisher


let publisher = car.$price


let subscriber2 : Subscribers.Sink<Int, Never>


subscriber2 = Subscribers.Sink(receiveCompletion: { print("completion \($0)")}) {
print("Option 2: value changed to \($0)")
}


publisher.subscribe(subscriber2)


//Assign a new value


car.price = 20


产出:

Option 1: value changed to 10
Option 2: value changed to 10
Option 1: value changed to 20
Option 2: value changed to 20

参考: