KVC 与 KVO 拾遗补缺

作者: SwiftCafe | 来源:发表于2016-01-09 22:39 被阅读589次

    KVC 和 KVO 是 Cocoa 框架提供的一个非常强的特性,使用好它们能大大提高我们的开发效率,今天咱们就来探讨一下关于 KVO 需要注意的事情。

    前一篇文章中,我们和大家一起分析了 KVC 的特性机制以及要注意的问题。建议大家先看一下这篇文章:漫谈 KVC 与 KVO 对 KVC 有一个了解。我们这次和大家分析 KVO 的相关内容。

    如何使用

    KVO 就是一种监听属性变化并作出响应的机制。这个特性在我们日常开发中应用很广泛,比如一个 UILabel 用于显示某个模型的属性值,当这个属性值改变的时候,自动更新 UILabel 的显示。

    KVO 的基本接口并不复杂,我们来看一个例子, 首先我们定义一个实体类:

    class Person: NSObject {
        
        dynamic var firstName: String
        dynamic var lastName: String
        
        init(firstName: String, lastName: String) {
            
            self.firstName = firstName
            self.lastName = lastName
            
        }
        
    }
    

    这个类有两个属性 firstName 和 lastName,它继承自 NSObject ,所以它对 KVC 协议提供了默认实现。注意一下这两个属性的 dynamic 标识。这个代表它支持 Objective-C Runtime 的动态分发机制,我们这里可以理解为 KVO 需要需要使用 Objective-C Runtime 的这个机制来实现属性更改的监听。 Swift 中的属性处于性能等方面的考虑默认是关闭动态分发的,所以我们这里面要显示的将属性用 dynamic 关键字标识出来。

    然后我们定义一个 ViewController, 将 KVO 所有的处理都写在这里:

    class Controller: UIViewController {
        
        var labelFirstName: UILabel?
        var labelLastName: UILabel?
        
        var person:Person?
        
        override func viewDidLoad() {
            
            self.labelFirstName = UILabel(frame: CGRectMake(0, 0, 0, 0))
            self.labelLastName = UILabel(frame: CGRectMake(0, 0, 0, 0))
            
            self.view.addSubview(self.labelFirstName!)
            self.view.addSubview(self.labelLastName!)
            
            self.person = Person(firstName: "peter", lastName: "cook")
            
            self.person?.addObserver(self, forKeyPath: "firstName", options: NSKeyValueObservingOptions.New, context: nil)
            self.person?.addObserver(self, forKeyPath: "lastName", options: NSKeyValueObservingOptions.New, context: nil)
            
        }
            
    }
    

    这个 ViewController 在它的 viewDidLoad 方法中初始化了两个 UILabel 和 Person 类,然后通过两个方法调用将 KVO 属性监听注册给这个 ViewController:

    self.person?.addObserver(self, forKeyPath: "firstName", options: NSKeyValueObservingOptions.New, context: nil)
    self.person?.addObserver(self, forKeyPath: "lastName", options: NSKeyValueObservingOptions.New, context: nil)
    

    addObserver 方法用于将实体类的属性注册给 KVO 监听对象。我们将 self.person 的两个属性 firstName 和 lastName(addObserver 方法的 forKeyPath 参数) 注册给了 self(addObserver 方法的第一个参数) ,然后我们指定了监听选项 NSKeyValueObservingOptions.New, 这个选项代表我们监听 KVO 每次属性改变后的新值。

    我们注册好了监听方法后,还需要在属性值改变的时候处理这个消息,这就需要我们的 ViewController 再实现一个 observeValueForKeyPath 方法:

    class Controller: UIViewController {
    
      override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
    
         if keyPath == "firstName" {
       
             if let firstName = change?[NSKeyValueChangeNewKey] as? String {
    
                 self.labelFirstName?.text = firstName
           
             }
       
         } else if keyPath == "lastName" {
       
             if let lastName = change?[NSKeyValueChangeNewKey] as? String {
           
                 self.labelLastName?.text = lastName
           
             }
       
         }
    
     }
       
    }
    

    每当检测到属性的改变,我们会判断对应的 keyPath,然后更新相应的 Label 的文本。

    addObserver

    addObserver 方法是 KVO 的一个关键方法,用来添加监听者与被监听者的关系。它的逻辑关系需要我们注意一下,我们是在被监听的对象上面调用这个方法,然后将监听者对象传递给这个方法,就像我们前面调用的一样:

    self.person?.addObserver(self, forKeyPath: "firstName", options: NSKeyValueObservingOptions.New, context: nil)
    

    第一个参数 self 就是监听者对象。然后还需要一个 keyPath 参数,它用于描述我们要监听哪个属性,比如我们这里监听 firstName 属性的改变。

    紧接着的 options 参数指定了监听选项,我们可以指定如下参数:

    • NSKeyValueObservingOptions.New 每次属性改变后的新值
    • NSKeyValueObservingOptions.Old 每次属性改变之前的旧值

    最后,在控制器被销毁的时候,我们要将 KVO 通知的删除掉(如果没有正确的清除掉 KVO 通知,程序可能会在某些时候以外的崩溃):

    deinit {
    
      super.viewDidLoad()
      self.person?.removeObserver(self, forKeyPath: "firstName")
      self.person?.removeObserver(self, forKeyPath: "lastName")
            
    }    
    

    这样, KVO 基本流程就完成了。

    属性依赖

    我们还会遇到这样的情况,比如有一个属性,它的值是依赖于另外的属性,还是以 Person 类为例,添加一个 fullName 属性:

    var fullName: String {
          
      get {
              
        return "\(lastName) \(firstName)"
              
      }
          
    }
    

    这个属性值是通过 lastName 和 firstName 这两个属性的值生成的。所以当这两个属性改变的时候,也相当于 fullName 的值也改变了。

    对于这样的属性关系,我们可以通过实现 keyPathsForValuesAffectingValueForKey 方法在实体类中声明属性依赖,以 fullName 属性为例:

    class Person: NSObject {
    
      override class func keyPathsForValuesAffectingValueForKey(key: String) -> Set<String> {
    
        if key == "fullName" {
        
            return Set<String>(arrayLiteral: "firstName","lastName")
        
        } else {
        
            return super.keyPathsForValuesAffectingValueForKey(key)
        
        }
            
      }
    
    }
    

    这样,我们声明了 fullName 属性依赖于两个其他属性 lastName,firstName。在 lastName 和 firstName 的属性值改变后,也会触发 fullName 属性改变的通知。

    注意:覆盖 keyPathsForValuesAffectingValueForKey 方法的时候有一点需要注意,这个方法在 Objective-C 中的签名是这样:+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key。如果照着这个逻辑,我们很可能在 Swift 中将这个方法的声明写成这样:override class func keyPathsForValuesAffectingValueForKey(key: NSString) -> NSSet。 注意这里面参数和返回值的类型,如果这样写,编译器就会报错。因为 Swift 中将 Objective-C 的一些基础类型都已经转变成了 Swift 的原生类型,所以我们这个方法签名要写成这样才可以通过编译: override class func keyPathsForValuesAffectingValueForKey(key: String) -> Set<String>

    自动通知与手动通知

    KVO 在默认情况下,只要为某个属性添加了监听对象,在这个属性值改变的时候,就会自动的通知监听者。也有一些情况下,可能我们想手动的处理这些通知的发送, KVO 也是允许我们这样做的。

    可以通过覆盖 automaticallyNotifiesObserversForKey 方法来告诉 KVO,那些属性是我们想手动处理的。 比如我们的 Person 类中,相对 firstName 进行处理,就在 automaticallyNotifiesObserversForKey 方法中对 firstName 这个 key 返回 false:

    class Person: NSObject {
    
      //... 
      override class func automaticallyNotifiesObserversForKey(key:String) -> Bool {
      
        if key == "firstName" {
              
          return false
              
        } else {
              
          return true
              
        }
          
      }
      //...
    
    }
    

    这样,我们在修改了 fistName 属性值后,就不会触发 KVO 的默认通知行为,是我们自己来控制通知的发送,我们需要修改 firstName 属性的实现来进行手工的通知发送:

    class Person: NSObject {
    
        var _firstName:String
    
        dynamic var firstName: String {
            
            set {
                
                self.willChangeValueForKey("firstName")
                _firstName = newValue
                self.didChangeValueForKey("firstName")
    
            }
    
            get{
                
                return _firstName
                
            }
            
        }
        
    }
    

    手动通知能让我们对 KVO 通知进行更细节的控制。但并不常用,大多数情况下使用 KVO 的自动通知机制就足够了。

    NSKeyValueObservingOptions.Initial

    我们在进行 UI 相关的 KVO 操作时候,通常会遇到这样的需求,在添加通知后,立即发送一个改变通知告诉 UI 去更新界面,这样让我们的界面有一个初始状态。我们可以指定 NSKeyValueObservingOptions.Initial 选项,这样在我们添加完 KVO 监听后,属性改变的通知就会立即被执行一次:

    self.person?.addObserver(self, forKeyPath: "firstName", options: [NSKeyValueObservingOptions.New,NSKeyValueObservingOptions.Initial], context: nil)
    

    NSKeyValueObservingOptions.Prior

    NSKeyValueObservingOptions.Prior 这个选项可以让我们在被监听的属性改变的时候得到两个通知,一个是在属性值改变之前,一个是属性值改变之后。然后就可以在 observeValueForKeyPath 中的 change 字典中以 NSKeyValueChangeNotificationIsPriorKey 键来表示当前通知是不是在属性被修改之前发送的:

    override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
            
            
      if let _ = change?[NSKeyValueChangeNotificationIsPriorKey] {
        
        print("old")
        
      }else {
      
        print("new")            
          
      }
      
            
    }
    

    当然,如果你只是想得到修改前和修改后的值,那么也可以用 change 字段中的 NSKeyValueChangeOldKey 和 NSKeyValueChangeNewKey 来得到相应的值:

    
    if let newFirstName = change?[NSKeyValueChangeNewKey] as? String {
                    
    }
    
    if let oldFirstName = change?[NSKeyValueChangeOldKey] as? String {
                    
    }
    
    

    总结

    KVO 是 Cocoa 提供的一个很强大的特性,但同时它也有很多坑需要我们注意,比如添加完监听后,要在不需要的时候删除掉监听,否则就会造成意外崩溃。对于有依赖关系的属性需要通过 keyPathsForValuesAffectingValueForKey 方法将依赖关系声明到实体类中。以及各个监听选项的作用。还有 Swift 中使用 KVO 特别要注意的那些地方。

    熟练使用 KVO 无疑会对我们的开发有很大的帮助,这篇文章我们将 KVO 大部分特性以及需要注意的地方总结了一下,当然算不上特别全面,但希望能通过它帮助大家拓展思路,有所帮助。

    更多精彩内容可关注微信公众号:
    swift-cafe

    相关文章

      网友评论

      本文标题:KVC 与 KVO 拾遗补缺

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