产品swift错误日志博客

KVC 与 KVO 拾遗补缺

2016-01-09  本文已影响589人  SwiftCafe

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 参数指定了监听选项,我们可以指定如下参数:

最后,在控制器被销毁的时候,我们要将 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

上一篇下一篇

猜你喜欢

热点阅读