美文网首页swift
Swift 5中的KVO指南和代码示例

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

作者: 摩卡奇 | 来源:发表于2020-12-15 11:41 被阅读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;
    
    • 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

    相关文章

      网友评论

        本文标题:Swift 5中的KVO指南和代码示例

        本文链接:https://www.haomeiwen.com/subject/gamigktx.html