IOS開發之旅-KVO

  在設計模式中,有一種模式稱爲觀察者模式,Objective-c也提供了類似的機制,簡稱爲KVO【Key-Value Observing】。當被觀察者的屬性改變時立即通知觀察者觸發響應的行爲。

  在KVO中,首先被觀察者與觀察者應該先建立關係,當被觀察的特定屬性改變時,立刻通知觀察者,建立聯繫調用如下方法:

/* Register or deregister as an observer of the value at a key path relative to the receiver. The options determine what is included in observer notifications and when they're sent, as described above, and the context is passed in observer notifications as described above. You should use -removeObserver:forKeyPath:context: instead of -removeObserver:forKeyPath: whenever possible because it allows you to more precisely specify your intent. When the same observer is registered for the same key path multiple times, but with different context pointers each time, -removeObserver:forKeyPath: has to guess at the context pointer when deciding what exactly to remove, and it can guess wrong.
*/
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;

  對參數options說明下,可以通過|操作符進行或操作,NSKeyValueObservingOptions定義如下:

typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
    NSKeyValueObservingOptionNew = 0x01,  //作爲變更信息的一部分發送新值
    NSKeyValueObservingOptionOld = 0x02,  //作爲變更信息的一部分發送舊值
    NSKeyValueObservingOptionInitial NS_ENUM_AVAILABLE(10_5, 2_0) = 0x04,  //在觀察者註冊時發送一個初始更新
    NSKeyValueObservingOptionPrior NS_ENUM_AVAILABLE(10_5, 2_0) = 0x08    //在變更前後分別發送變更,而不只在變更後發送一次
};

  addObserver的context參數與observeValueForKeyPath的context參數指向同一個地址,進行額外參數的傳遞。

  當不在需要觀察者監聽被觀察者的屬性變化時,必須移除這種監聽關係,否則程序會拋出異常,調用如下方法移除監聽關係:

- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(void *)context NS_AVAILABLE(10_7, 5_0);
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

  當觀察者接受到被觀察者屬性改變的通知時,立即調用特定的方法以示響應,觀察者必須實現如下方法:

/* Given that the receiver has been registered as an observer of the value at a key path relative to an object, be notified of a change to that value.

The change dictionary always contains an NSKeyValueChangeKindKey entry whose value is an NSNumber wrapping an NSKeyValueChange (use -[NSNumber unsignedIntegerValue]). The meaning of NSKeyValueChange depends on what sort of property is identified by the key path:
    - For any sort of property (attribute, to-one relationship, or ordered or unordered to-many relationship) NSKeyValueChangeSetting indicates that the observed object has received a -setValue:forKey: message, or that the key-value coding-compliant set method for the key has been invoked, or that a -willChangeValueForKey:/-didChangeValueForKey: pair has otherwise been invoked.
    - For an _ordered_ to-many relationship, NSKeyValueChangeInsertion, NSKeyValueChangeRemoval, and NSKeyValueChangeReplacement indicate that a mutating message has been sent to the array returned by a -mutableArrayValueForKey: message sent to the object, or sent to the ordered set returned by a -mutableOrderedSetValueForKey: message sent to the object, or that one of the key-value coding-compliant array or ordered set mutation methods for the key has been invoked, or that a -willChange:valuesAtIndexes:forKey:/-didChange:valuesAtIndexes:forKey: pair has otherwise been invoked.
    - For an _unordered_ to-many relationship (introduced in Mac OS 10.4), NSKeyValueChangeInsertion, NSKeyValueChangeRemoval, and NSKeyValueChangeReplacement indicate that a mutating message has been sent to the set returned by a -mutableSetValueForKey: message sent to the object, or that one of the key-value coding-compliant set mutation methods for the key has been invoked, or that a -willChangeValueForKey:withSetMutation:usingObjects:/-didChangeValueForKey:withSetMutation:usingObjects: pair has otherwise been invoked.

For any sort of property, the change dictionary contains an NSKeyValueChangeNewKey entry if NSKeyValueObservingOptionNew was specified at observer registration time, it's the right kind of change, and this isn't a prior notification. The change dictionary contains an NSKeyValueChangeOldKey if NSKeyValueObservingOptionOld was specified and it's the right kind of change. See the comments for the NSKeyValueObserverNotification informal protocol methods for what the values of those entries can be.

For an _ordered_ to-many relationship, the change dictionary always contains an NSKeyValueChangeIndexesKey entry whose value is an NSIndexSet containing the indexes of the inserted, removed, or replaced objects, unless the change is an NSKeyValueChangeSetting.

If NSKeyValueObservingOptionPrior (introduced in Mac OS 10.5) was specified at observer registration time, and this notification is one being sent prior to a change as a result, the change dictionary contains an NSKeyValueChangeNotificationIsPriorKey entry whose value is an NSNumber wrapping YES (use -[NSNumber boolValue]).

context is always the same pointer that was passed in at observer registration time.
*/
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;

  對於observeValueForKeyPath的字典類型的change參數的key可能包含如下的值,具體包含什麼key,是由調用addObserver時設置的options參數的值

FOUNDATION_EXPORT NSString *const NSKeyValueChangeKindKey;
FOUNDATION_EXPORT NSString *const NSKeyValueChangeNewKey;
FOUNDATION_EXPORT NSString *const NSKeyValueChangeOldKey;
FOUNDATION_EXPORT NSString *const NSKeyValueChangeIndexesKey;
FOUNDATION_EXPORT NSString *const NSKeyValueChangeNotificationIsPriorKey NS_AVAILABLE(10_5, 2_0);

  NSKeyValueChangeKindKey可能取如下的枚舉值:

/* Possible values in the NSKeyValueChangeKindKey entry in change dictionaries. See the comments for -observeValueForKeyPath:ofObject:change:context: for more information.
*/
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
    NSKeyValueChangeSetting = 1,
    NSKeyValueChangeInsertion = 2,
    NSKeyValueChangeRemoval = 3,
    NSKeyValueChangeReplacement = 4
};

  以上介紹只針對單屬性的監聽,有時候可能會遇到組合屬性的監聽,比如說一個用戶的fullName由firstName、lastName構成,那麼不管是firstName還是lastName被改變了,fullName都會跟隨改變,如果觀察者監聽了fullName,那麼firstName、lastName改變時候都應該觸發觀察者的observeValueForKeyPath方法,那麼如何在firstName或者lastName改變的時候自動觸發觀察者的observeValueForKeyPath方法?觀察者必須實現如下方法

/* Return a set of key paths for properties whose values affect the value of the keyed property. When an observer for the key is registered with an instance of the receiving class, KVO itself automatically observes all of the key paths for the same instance, and sends change notifications for the key to the observer when the value for any of those key paths changes. The default implementation of this method searches the receiving class for a method whose name matches the pattern +keyPathsForValuesAffecting<Key>, and returns the result of invoking that method if it is found. So, any such method must return an NSSet too. If no such method is found, an NSSet that is computed from information provided by previous invocations of the now-deprecated +setKeys:triggerChangeNotificationsForDependentKey: method is returned, for backward binary compatibility.

This method and KVO's automatic use of it comprise a dependency mechanism that you can use instead of sending -willChangeValueForKey:/-didChangeValueForKey: messages for dependent, computed, properties.
 
You can override this method when the getter method of one of your properties computes a value to return using the values of other properties, including those that are located by key paths. Your override should typically invoke super and return a set that includes any members in the set that result from doing that (so as not to interfere with overrides of this method in superclasses).

You can't really override this method when you add a computed property to an existing class using a category, because you're not supposed to override methods in categories. In that case, implement a matching +keyPathsForValuesAffecting<Key> to take advantage of this mechanism.
*/
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key

或者

+(NSSet*)keyPathsForValuesAffecting<Key>

  

  下面用生活中得一個例子來演示,當用戶銀行中得餘額發生改變時應當立即通知用戶改變情況,

  KVOBankObject:

#import <Foundation/Foundation.h>

@interface BankObject : NSObject
{
    int _accountBalance;
}

@property int accountBalance;

-(id)initWithAccountBalance:(int)accountBalance;

@end


#import "KVOBankObject.h" @implementation BankObject -(id)initWithAccountBalance:(int)accountBalance { if(self = [super init]) { self.accountBalance = accountBalance; } return self; } @end

  KVOPersonObject:

#import <Foundation/Foundation.h>

@interface PersonObject : NSObject
@property (nonatomic,strong) NSString* firstName;
@property (nonatomic,strong) NSString* lastName;
-(id)initWithFirstName:(NSString*)firstName LastName:(NSString*)lastName;
-(NSString*)fullName;
@end



#import "KVOPersonObject.h"
#import "KVOBankObject.h"

@implementation PersonObject

-(id)initWithFirstName:(NSString *)firstName LastName:(NSString *)lastName
{
    if(self = [super init])
    {
        self.firstName = firstName;
        self.lastName = lastName;
    }
    return self;
}

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    
    if ([object isKindOfClass:[BankObject class]]) {
        if([keyPath isEqualToString:NSStringFromSelector(@selector(accountBalance))])
        {
            NSLog(@"%@",change);
        }
    }
    
    if ([object isKindOfClass:[PersonObject class]]) {
        if([keyPath isEqualToString:NSStringFromSelector(@selector(fullName))])
        {
            NSLog(@"%@",change);
        }
    }
}

-(NSString*)fullName
{
    return [NSString stringWithFormat:@"%@ %@",self.firstName,self.lastName];
}

+(NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key
{
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if([key isEqualToString:NSStringFromSelector(@selector(fullName))])
    {
        NSArray *affectingKeys = @[NSStringFromSelector(@selector(lastName)),NSStringFromSelector(@selector(firstName))];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}

+(NSSet *)keyPathsForValuesAffectingFullName
{
    return [NSSet setWithObjects:NSStringFromSelector(@selector(lastName)),NSStringFromSelector(@selector(firstName)), nil];
}
@end

 

 調用示例

#import <Foundation/Foundation.h>
#import "KVOBankObject.h"
#import "KVOPersonObject.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        PersonObject *personObj = [[PersonObject alloc]init];
        BankObject *bankObj = [[BankObject alloc]initWithAccountBalance:1];
        [bankObj addObserver:personObj forKeyPath:@"accountBalance" options: NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
        bankObj.accountBalance = 2;
        [bankObj removeObserver:personObj forKeyPath:@"accountBalance"];
    }
    return 0;
}

  因爲options : NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew,所以輸出屬性的舊值與新值,如下所示:

{
    kind = 1;
    new = 2;
    old = 1;
}

  如果設置options: NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial。NSKeyValueObservingOptionInitial在觀察者註冊時發送一個初始更新,輸出如下:

//註冊時發送一個更新請求
{
    kind = 1;
    new = 1;
}
//值改變之後發送一個更新請求
{
    kind = 1;
    new = 2;
    old = 1;
}

  如果設置options: NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew | NSKeyValueObservingOptionPrior。NSKeyValueObservingOptionPrior在變更前後分別發送變更,而不只在變更後發送一次。輸出如下:

//變更前發送一個請求
{ kind
= 1; notificationIsPrior = 1; old = 1; }
//變更後發送一個請求
{ kind = 1; new = 2; old = 1; }

  下面演示組合屬性fullName的監聽,當firstName、lastName改變時,觸發監聽方法

#import <Foundation/Foundation.h>
#import "KVOBankObject.h"
#import "KVOPersonObject.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        PersonObject *personObj = [[PersonObject alloc]initWithFirstName:@"quan" LastName:@"long"];
        [personObj addObserver:personObj forKeyPath:@"fullName" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
        personObj.firstName = @"pstune";    //觸發一次監聽
        personObj.lastName  = @".com";      //觸發一次監聽
        [personObj removeObserver:personObj forKeyPath:@"fullName"];
        
        
        /*
        BankObject *bankObj = [[BankObject alloc]initWithAccountBalance:1];
        [bankObj addObserver:personObj forKeyPath:@"accountBalance" options: NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew | NSKeyValueObservingOptionPrior context:nil];
        bankObj.accountBalance = 2;
        [bankObj removeObserver:personObj forKeyPath:@"accountBalance"];
         */
    }
    return 0;
}

  以上算是對KVO的簡單總結,如有不正確的地方,請給我留言。

 

  

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章