Swift 5中的KVO指南和代码示例

概念

KVO意思是键值观察,它是观察Objective-C和Swift中可用的程序状态变化的技术之一。

这个概念很简单:当我们有一个带有一些实例变量的对象时,KVO允许其他对象对任何这些实例变量的更改进行监视。

KVO是观察者模式的实际示例。使Objective-C(和Obj-C桥接的Swift)与众不同的原因是,您添加到类中的每个实例变量都可以通过KVO立即观察到! (此规则有一些例外,我将在文章中讨论它们)。

但是在大多数其他编程语言中,这种工具并不是开箱即用的-您通常需要在变量的设置器中编写其他代码,以将值更改通知观察者。

Swift已从Objective-C继承了KVO,因此,要全面了解,您需要了解KVO在Objective-C中的工作方式。

KVO in Objective-C

现在我们有一个名为Person的类,其属性nameage

@interface Person: NSObject
 
@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) NSInteger age;
 
@end

现在,此类的对象可以通过KVO传达属性的更改,且无需其他代码!

因此,我们唯一需要做的就是在另一个类中开始观察:

@implementation SomeOtherClass

- (void)observeChanges:(Person *)person {
    [person addObserver:self
             forKeyPath:@"age"
                options:NSKeyValueObservingOptionNew
                context:nil];
}

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSKeyValueChangeKey,id> *)change
                       context:(void *)context {
    if ([keyPath isEqualToString:@"age"]) {
        NSNumber *ageNumber = change[NSKeyValueChangeNewKey];
        NSInteger age = [ageNumber integerValue];
        NSLog(@"New age is: %@", age);
    }
}

@end

现在,每次Person上的age属性发生变化时,我们能看到New age is: ...从观察者打印到日志上。

如您所见,KVO交互涉及两种方法。

第一个是addObserver:forKeyPath:options:context:,可以在任何NSObject上调用它,包括Person。此方法将观察者附加到对象。

第二个是observeValueForKeyPath:ofObject:change:context:这是NSObject中的另一个标准方法,我们必须在观察者的类中覆盖它。此方法用于接收观察通知。

第三种方法removeObserver:forKeyPath:context:允许您停止观察。如果观察到的对象的生命周期超过观察者,请务必取消订阅通知。因此,只需在观察者的dealloc方法中删除订阅即可。

现在,让我们讨论一下KVO中使用的方法的参数。

我们用于附加观察者的方法在NSObject中声明

- (void)addObserver:(NSObject *)observer 
         forKeyPath:(NSString *)keyPath
            options:(NSKeyValueObservingOptions)options
            context:(nullable void *)context;
  • observer是将接收更改通知的对象。通常,您在此参数中提供self,因为addObserver:是从自己的实例方法内部调用的。
  • keyPath是一个字符串参数,在最简单的情况下,它只是您要观察的属性的名称。如果属性引用了复杂的对象层次结构,则可以是用于挖掘该层次结构的一组属性名称:"person.father.age"
  • options是一个枚举,可用于自定义随通知一起传递的信息以及应在何时发送的信息。可用选项为NSKeyValueObservingOptionNewNSKeyValueObservingOptionOld,它们分别控制是否包含新值和旧值。还有NSKeyValueObservingOptionInitial用于在订阅后立即触发通知,还有NSKeyValueObservingOptionPrior用于区分集合中的更改,例如在NSArray中删除的插入。
  • context是对任意类的对象的引用,这有助于在某些复杂的用例中(例如在使用CoreData时)识别预订。在大多数其他情况下,您只需在此处提供nil即可。

我们用于处理更新通知的方法

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSKeyValueChangeKey,id> *)change
                       context:(void *)context
  • keyPath与调用观察者时提供的字符串值相同。您可能会问为什么这里也提供它。原因是我们可能一次观察多个属性,因此可以使用此参数将一个属性的通知与另一个属性的通知区分开。
  • object是观察对象。由于我们可以观察到多个对象的变化,因此该参数使我们能够确定谁的属性发生了变化。
  • change是字典,其中包含有关更改后的值的信息。基于我们在订阅时提供的NSKeyValueObservingOptions,该词典可能包含键NSKeyValueChangeNewKey下的当前值,NSKeyValueChangeOldKey的旧值以及观察集合中的更改时的"diff"信息:NSKeyValueChangeIndexIndexesKeyNSKeyValueChangeKindKey
  • ** context**是订阅时提供的参考。同样,用于正确的观察识别,在大多数情况下可以忽略

当KVO不起作用时

尽管KVO看起来像魔术,但背后没有任何非凡之处。实际上,您可以直接访问其内部,默认情况下它们是隐藏的。

诀窍是Objective-C如何生成属性的设置器。当你声明一个属性:

@property (nonatomic, assign) NSInteger age;

由Objective-C生成的事实设置器等效于以下内容:

- (void)setAge:(NSInteger)age {
    [self willChangeValueForKey:@"age"];
    _age = age;
    [self didChangeValueForKey:@"age"];
}

而且,如果您显式定义了setter而不调用这些willChangeValueForKeydidChangeValueForKey

- (void)setAge:(NSInteger)age {
    _age = age;
}

…KVO将停止为该属性工作。

因此,基本上,这两种方法willChangeValueForKeydidChangeValueForKey允许KVO将更新发送给订阅者,并且开发人员可以通过省略setter的这些调用来屏蔽。

重要的是要了解由Objective-C合成的每个@property都会添加一个带有_前缀的隐藏实例变量。

例如,@property NSInteger age;生成名称为_age的实例变量,该变量可以像该属性一样进行访问:

self.age = 25;
self._age = 25;

区别在于self.age = 25;触发setter的setAge:,而self._age = 25;直接更改存储的变量。

摆脱KVO的另一种方法是首先不使用@property,而是将实例变量存储在该类的匿名类别中:

@interface Person () {
    NSInteger _privateVariable;
}
@end

对于此类变量,Objective-C不会生成setter和getter,因此无法启用KVO。

Swift中的KVO

Swift从Objective-C继承了对KVO的支持,但是与后者不同,默认情况下,Swift类中禁用了KVO。

Swift中使用的Objective-C类保持启用KVO,但对于Swift类,我们需要将基类设置为NSObject并在变量中添加@objc动态属性:

class Person: NSObject {
    @objc dynamic var age: Int
    @objc dynamic var name: String
}

Swift中有两种用于键值观察的API:旧的API来自Objective-C,而新的API更加灵活,安全且对Swift友好。

让我们从新的API开始:

class PersonObserver {

    var kvoToken: NSKeyValueObservation?
    
    func observe(person: Person) {
        kvoToken = person.observe(\.age, options: .new) { (person, change) in
            guard let age = change.new else { return }
            print("New age is: \(age)")
        }
    }
    
    deinit {
        kvoToken?.invalidate()
    }
}

如您所见,新API在订阅开始的地方使用闭包回调传递更改通知。

这样更加方便和安全,因为我们不再需要检查keyPathobjectcontext,在该闭包中,没有其他通知会发送,仅是我们已订阅的通知。

这里有一种管理观察生命周期的新方法-订阅操作将返回NSKeyValueObservation类型的token,该token必须存储在某个位置,例如,在观察者类的实例变量中。

稍后,我们可以对该token调用invalidate()以停止观察,就像上面的deinit方法一样。

最终更改与keyPath有关。 String容易出错,因为在重命名变量时,编译器将无法告诉您keyPath现在导致无处可去。取而代之的是,此新API使用Swift的特殊类型作为keyPath,这使编译器可以验证路径是否有效。

options参数具有与Objective-C中相同的选项集。如果需要提供多个选项,只需将它们捆绑在一个数组中即可:options: [.new, .old]

虽然保留了所有缺点,但也可以使用旧的API,因此建议您改用新的API。

这是旧的:

class PersonObserver: NSObject {
    
    func observe(person: Person) {
        person.addObserver(self, forKeyPath: "age",
                           options: .new, context: nil)
    }
    
    override func observeValue(forKeyPath keyPath: String?,
                               of object: Any?,
                               change: [NSKeyValueChangeKey : Any]?,
                               context: UnsafeMutableRawPointer?) {
        if keyPath == "age",
           let age = change?[.newKey] {
             print("New age is: \(age)")
        }
    }
}

旧的API要求观察者也必须是NSObject的子类。我们还需要验证keyPathobjectcontext,因为其他通知也以这种方法传递,就像在Objective-C中一样。

KVO替代品

现代iOS开发中还有很多其他技术可以达到相同的目的:状态更改的传播。我不得不说,KVO是最不常用的一种,因为替代品在便利性和多功能性方面往往超过其。实际上,我写了一系列文章,涵盖了状态更改传播的所有可用工具,并详尽地描述了每种工具的利弊。以下是快速参考:

该系列文章的最后一篇是最终指南,我在其中描述一种工具比另一种工具更适合的实际情况。

最后,随着Apple的Combine框架的发布,KVO现在没有机会保持其最初的知名度,但是了解其工作原理仍然很重要!

翻译来自:https://nalexn.github.io/kvo-guide-for-key-value-observing/#kvo_swift

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容