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


174

如果是这样,那么在Objective-C中使用键值观察时是否没有其他键差异?


2
一个示例项目演示了如何通过Swift在UIKit界面中使用KVO:github.com/jameswomack/kvo-in-swift
james_womack 2014年

@JanDvorak请参阅《KVO编程指南》,它是该主题的不错介绍。
罗布2014年

1
尽管不是您问题的答案,但是您也可以使用didset()函数开始执行操作。
文森特

请注意,使用时会出现Swift4 错误.initial。有关解决方案,请参见此处。我强烈建议您查看Apple文档。它最近已更新,涵盖了许多重要说明。另请参阅Rob的其他答案
蜂蜜

Answers:


107

(编辑以添加新信息):考虑使用Combine框架是否可以帮助您完成所需的工作,而不是使用KVO

是的,没有。KVO一直以来都在处理NSObject子类。它不适用于不继承NSObject的类。斯威夫特(目前至少)没有自己的原生观察系统。

(有关如何将其他属性公开为ObjC的信息,请参见注释,以便KVO在它们上起作用)

有关完整示例,请参阅Apple文档


74
从Xcode 6 beta 5开始,您可以dynamic在任何Swift类上使用关键字来启用KVO支持。
fabb

7
万岁@fabb!为了清楚起见,dynamic关键字位于要使键值可观察的属性上。
杰里

5
对于该解释dynamic的关键字可以在苹果开发者图书馆被发现使用斯威夫特与可可和Objective-C部分
Imanou Petit 2014年

6
由于@fabb的注释对我来说还不清楚,因此:dynamic对于希望与KVO兼容的类中的任何属性,请使用关键字(而不是dynamic类本身的关键字)。这对我有用!
2014年

1
并不是的; 您不能从“外部”注册新的didSet,它在编译时必须是该类型的一部分。
Catfish_Man

155

您可以在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中,这已大大简化,如在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中,我们现在使用反斜杠字符(这\.barbar所观察对象的属性的键路径)对键路径进行强类型输入。另外,由于它使用了完成关闭模式,因此我们不必手动删除观察者(当token落入范围之外时,将为我们移除观察者),也不必担心super如果键没有调用实现,比赛。仅当调用此特定观察者时,才调用该闭包。有关更多信息,请参阅WWDC 2017视频,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实现的讨论,请参阅下面的原始答案。


使用Swift和Cocoa和Objective-C指南的“ 采用可可设计约定”一章的键值观察”部分中描述了如何使用dynamic关键字通过NSObject子类实现KVO :

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

  1. dynamic修饰符添加到要观察的任何属性。有关更多信息dynamic,请参见要求动态调度

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

    private var myContext = 0
  3. 为键路径添加观察者,并覆盖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讨论随后已从“ 将Swift与Cocoa和Objective-C结合使用”指南中删除,该指南已针对Swift 3进行了改编,但仍按此答案的顶部概述进行。]


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


目的是什么myContext?如何观察多个属性?
devth 2014年

1
根据KVO编程指南:“当你注册一个对象作为一个观察者,也可以提供一个context指针的。context当指针被提供给观察者observeValueForKeyPath:ofObject:change:context:被调用的。context指针可以是C指针或对象引用的。context指针可以是用作唯一标识符以确定正在观察的变化,或向观察者提供其他数据。”
罗布

您需要删除deinit中的观察者
Jacky

3
据我了解,@ devth如果子类或超类也为同一变量注册了KVO观察者,则将多次调用observeValueForKeyPath。在这种情况下,可以使用上下文来区分自己的通知。有关此内容的更多信息:dribin.org/dave/blog/archives/2008/09/24/proper_kvo_usage
Zmey 2015年

1
如果保留为options空,则仅表示change将不包含旧值或新值(例如,您可以通过引用对象本身来自己获得新值)。如果您只指定了.newnot .old,则表示change仅包含新值,而不包括旧值(例如,您通常不关心旧值是什么,而只关心新值)。如果您需要observeValueForKeyPath同时传递旧值和新值,请指定[.new, .old]。最下面的一行options仅指定change字典中包含的内容。
罗布

92

是和否:

  • 是的,您可以在Swift中使用相同的旧KVO API来观察Objective-C对象。
    您还可以观察dynamic继承自的Swift对象的属性NSObject
    但是... 不,它的类型不是很严格,就像您期望的Swift本机观测系统那样。
    将Swift与Cocoa和Objective-C结合使用 关键价值观察

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

  • 是的,有内置的Property Observers,它们是强类型的。
    但是... 不,它们不是KVO,因为它们仅允许观察对象自身的属性,不支持嵌套观察(“键路径”),因此您必须显式实现它们。
    Swift编程语言 物业观察员

  • 是的,您可以实现显式的值观察,该观察将被强类型化,并允许从其他对象添加多个处理程序,甚至支持嵌套/“键路径”。
    但是... 不,它不是KVO,因为它仅适用于您实现为可观察的属性。
    您可以在此处找到用于实现此类价值观察的库:
    Observable-Swift-Swift的KVO-价值观察和事件


10

一个示例可能会有所帮助。如果我有一个带有属性model的类的实例,并且可以使用以下方法观察这些属性:Modelnamestate

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

2
如果我没记错的话,observeValueForKeyPath调用方法适用于Swift2。
Fattie

9

是。

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

dynamic var foo = 0

dynamic修改确保对声明的引用将被动态调度和访问过objc_msgSend


7

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

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

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

使用willChangeValueForKeydidChangeValueForKey来自NSKeyValueObserving

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

使用dynamic。查看Swift类型兼容性

如果使用的API(例如键值观察)可以动态替换方法的实现,则还可以使用dynamic修饰符要求通过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
        }
    }
}

5

目前,Swift不支持任何用于观察“ self”以外对象的属性变化的内置机制,因此不,它不支持KVO。

但是,KVO是Objective-C和Cocoa的基本组成部分,因此很有可能会在将来添加它。当前文档似乎暗示了这一点:

键值观察

即将发布的信息。

将Swift与Cocoa和Objective-C结合使用


2
显然,您现在参考的指南描述了如何在Swift中执行KVO。
罗布

4
是的,现已实施,2014
Max MacLeod

5

总览

Combine不使用NSObject或也可以使用Objective-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

参考:


4

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

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

2
在Xcode beta 6中,它要求:重写func watchValueForKeyPath(keyPath:String ?, ofObject object:AnyObject ?,更改:[String:AnyObject] ?,上下文:UnsafeMutablePointer <Void>)
hcanfly

4

这可能对少数人有帮助-

// 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。您只需很少的更改就可以使用此代码。


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

}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.