Swift 中是不是提供键值观察 (KVO)?

Posted

技术标签:

【中文标题】Swift 中是不是提供键值观察 (KVO)?【英文标题】:Is key-value observation (KVO) available in Swift?Swift 中是否提供键值观察 (KVO)? 【发布时间】:2014-07-28 08:11:01 【问题描述】:

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

【问题讨论】:

一个示例项目,演示通过 Swift 在 UIKit 界面中使用 KVO:github.com/jameswomack/kvo-in-swift @JanDvorak 请参阅KVO Programming Guide,这是对该主题的一个很好的介绍。 虽然不能回答您的问题,但您也可以使用 didset() 函数开始操作。 请注意,当您使用.initial 时,会有一个 Swift4 错误。有关解决方案,请参阅here。我强烈建议您查看Apple docs。它最近已更新,涵盖了许多重要说明。另请参阅 Rob 的other answer 【参考方案1】:

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

class Foo: NSObject 
    @objc dynamic var bar = 0

然后您可以注册以观察bar 属性的变化。在 Swift 4 和 Swift 3.2 中,这已大大简化,如 Using Key-Value Observing in 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 视频,What's New in Foundation。

在 Swift 3 中,观察这一点,它有点复杂,但与在 Objective-C 中所做的非常相似。也就是说,您将实现observeValue(forKeyPath keyPath:, of object:, change:, context:),它 (a) 确保我们正在处理我们的上下文(而不是我们的 super 实例已注册观察的内容);然后 (b) 根据需要处理它或将其传递给 super 实现。并确保在适当的时候将自己作为观察者移除。例如,你可以在观察者被释放时移除它:

在 Swift 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 在Using Swift with Cocoa and Objective 的Adopting Cocoa Design Conventions 章节的Key-Value Observing 部分进行了描述-C 指南:

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

    dynamic 修饰符添加到您要观察的任何属性。有关dynamic 的更多信息,请参阅Requiring Dynamic Dispatch。

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

    创建一个全局上下文变量。

    private var myContext = 0
    

    为key-path添加一个观察者,并覆盖observeValueForKeyPath:ofObject:change:context:方法,并移除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)
        
    
    

[注意,此 KVO 讨论随后已从 Using Swift with Cocoa and Objective-C 指南中删除,该指南已针对 Swift 3 进行了调整,但它仍然可以按照这个答案。]


值得注意的是,Swift 有自己的本机 property observer 系统,但这是针对指定其自己代码的类,该代码将在观察其自身属性时执行。另一方面,KVO 旨在注册以观察其他类的某些动态属性的变化。

【讨论】:

myContext 的目的是什么,如何观察多个属性? 根据KVO Programming Guide:“当你将一个对象注册为观察者时,你也可以提供一个context指针。context指针是在调用observeValueForKeyPath:ofObject:change:context:时提供给观察者的.context 指针可以是 C 指针或对象引用。context 指针可以用作唯一标识符来确定正在观察的更改,或向观察者提供一些其他数据。"跨度> 你需要在 deinit 中移除观察者 @devth,据我了解,如果子类或超类也为同一个变量注册 KVO 观察者,observeValueForKeyPath 将被多次调用。在这种情况下,可以使用上下文来区分自己的通知。更多信息:dribin.org/dave/blog/archives/2008/09/24/proper_kvo_usage 如果您将options 留空,这仅意味着change 将不包含旧值或新值(例如,您可能只是通过引用对象本身来获取新值)。如果您只指定.new 而不是.old,则意味着change 将只包含新值,而不包含旧值(例如,您通常不关心旧值是什么,而只关心新值)。如果您需要observeValueForKeyPath 将旧值和新值都传递给您,请指定[.new, .old]。最重要的是,options 只是指定了 change 字典中包含的内容。【参考方案2】:

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

是和不是。 KVO 像往常一样在 NSObject 子类上工作。它不适用于不继承 NSObject 的类。 Swift(至少目前)没有自己的原生观察系统。

(请参阅 cmets 了解如何将其他属性公开为 ObjC 以便 KVO 对其进行处理)

有关完整示例,请参阅Apple Documentation。

【讨论】:

从 Xcode 6 beta 5 开始,您可以在任何 Swift 类上使用 dynamic 关键字来启用 KVO 支持。 @fabb 万岁!为清楚起见,dynamic 关键字位于您要使键值可观察的属性上。 dynamic关键字的解释可以在Apple Developer Library的Using Swift with Cocoa and Objective-C section中找到。 因为我从 @fabb 的评论中并不清楚这一点:对您希望符合 KVO 的类中的任何 properties 使用 dynamic 关键字(不是类本身的dynamic 关键字)。这对我有用! 不是真的;您不能从“外部”注册新的 didSet,它必须在编译时成为该类型的一部分。【参考方案3】:

是和否:

是的,您可以在 Swift 中使用相同的旧 KVO API 来观察 Objective-C 对象。 您还可以观察继承自 NSObject 的 Swift 对象的 dynamic 属性。 但是...它不像你所期望的那样是强类型的。Using Swift with Cocoa and Objective-C | Key Value Observing

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

是的,有内置的Property Observers,它们是强类型的。 但是...它们不是 KVO,因为它们只允许观察对象自身的属性,不支持嵌套观察(“关键路径”),并且您必须显式地实现它们。The Swift Programming Language | Property Observers

是的,您可以实现显式值观察,这将是强类型的,并允许从其他对象添加多个处理程序,甚至支持嵌套/“键路径”。 但是...它不会是 KVO,因为它只适用于您实现为可观察的属性。 您可以在此处找到实现此类值观察的库:Observable-Swift - KVO for Swift - Value Observing and Events

【讨论】:

【参考方案4】:

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

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)")

【讨论】:

如果我没记错的话,observeValueForKeyPath的调用方式是针对Swift2的。【参考方案5】:

是的。

KVO 需要动态调度,因此您只需将 dynamic 修饰符添加到方法、属性、下标或初始化程序:

dynamic var foo = 0

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

【讨论】:

【参考方案6】:

除了 Rob 的回答。该类必须继承自NSObject,并且我们有3种方法来触发属性更改

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

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

使用来自NSKeyValueObservingwillChangeValueForKeydidChangeValueForKey

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

使用dynamic。见Swift Type Compatibility

如果您使用动态替换方法实现的键值观察等 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
        
    

【讨论】:

【参考方案7】:

目前 Swift 不支持任何内置机制来观察除 'self' 之外的对象的属性变化,所以不,它不支持 KVO。

然而,KVO 是 Objective-C 和 Cocoa 的一个基础部分,它似乎很有可能在未来被添加。当前的文档似乎暗示了这一点:

键值观察

信息即将发布。

Using Swift with Cocoa and Objective-C

【讨论】:

显然,您参考的指南现在描述了如何在 Swift 中进行 KVO。 是的,从 2014 年 9 月开始实施【参考方案8】:

需要提及的重要一点是,将 Xcode 更新到 7 beta 后,您可能会收到以下消息: “方法不会覆盖其超类中的任何方法”。这是因为参数的可选性。确保您的观察处理程序如下所示:

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

【讨论】:

在 Xcode beta 6 中它需要:覆盖 func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer)【参考方案9】:

这可能对少数人有帮助 -

// 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。您只需稍作改动即可使用此代码。

【讨论】:

【参考方案10】:

概述

可以使用Combine而不使用NSObjectObjective-C

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

注意:只需要用于类而不是值类型。

代码:

Swift 版本: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

参考:

https://developer.apple.com/documentation/combine https://developer.apple.com/documentation/combine/receiving_and_handling_events_with_combine https://developer.apple.com/documentation/combine/published

【讨论】:

iOS 13 +?其他人被忽略了?【参考方案11】:

对于遇到诸如 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)")
   


【讨论】:

以上是关于Swift 中是不是提供键值观察 (KVO)?的主要内容,如果未能解决你的问题,请参考以下文章

Rx 键值观察KVO的使用

深入理解 KVCKVO 实现机制 — KVO

KVO-理解与简单使用

[crash详解与防护] KVO crash

iOS 设计模式(五)-KVO 详解

NotificationCenter KVC KVO Delegate总结