swift

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

2020-12-15  本文已影响0人  摩卡奇

概念

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;

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

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSKeyValueChangeKey,id> *)change
                       context:(void *)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

上一篇 下一篇

猜你喜欢

热点阅读