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

时间:2014-06-07 00:05:39

标签: swift key-value-observing

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

11 个答案:

答案 0 :(得分:139)

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

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,在Adopting Cocoa Design Conventions章节的键值观察部分中进行了描述使用Swift与Cocoa和Objective-C 指南:

  

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

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

    class MyObjectToObserve: NSObject {
        dynamic var myDate = NSDate()
        func updateDate() {
            myDate = NSDate()
        }
    }
    
  2.   
  3. 创建全局上下文变量。

    private var myContext = 0
    
  4.   
  5. 为关键路径添加观察者,并覆盖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)
        }
    }
    
  6.   

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


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

答案 1 :(得分:101)

是和否。 KVO对NSObject子类的工作原理与它一如既往。它不适用于没有NSObject子类的类。斯威夫特(目前至少)没有自己的原生观察系统。

(请参阅有关如何将其他属性公开为ObjC的注释,以便KVO对其进行处理)

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

答案 2 :(得分:91)

是和否:

  • ,您可以在Swift中使用相同的旧KVO API来观察Objective-C对象。
    您还可以观察从dynamic继承的Swift对象的NSObject属性 但是...... 没有它没有强烈打字,因为你可以期待Swift原生观察系统。
    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

答案 3 :(得分:10)

一个例子可能对此有所帮助。如果我有一个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)")
}

答案 4 :(得分:8)

KVO需要动态调度,因此您只需将dynamic修饰符添加到方法,属性,下标或初始值设定项中:

dynamic var foo = 0

dynamic修饰符可确保通过objc_msgSend动态调度和访问对声明的引用。

答案 5 :(得分:5)

目前Swift不支持任何内置机制来观察“self”以外的对象的属性更改,所以不,它不支持KVO。

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

  

键值观察

     

即将发布的信息。

Using Swift with Cocoa and Objective-C

答案 6 :(得分:5)

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

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

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

使用willChangeValueForKey

中的didChangeValueForKeyNSKeyValueObserving
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 :(得分:4)

需要提及的一件重要事项是,在将 Xcode 更新为 7 beta 后,您可能会收到以下消息: “方法不会覆盖其超类中的任何方法”。那是因为论证的选择性。确保您的观察处理程序看起来完全如下:

./

答案 8 :(得分:3)

这可能对少数人有用 -

// 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。您可以使用此代码进行少量更改。

答案 9 :(得分:1)

遇到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)")
   }

}

答案 10 :(得分:1)

概述

可以使用 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

引用: