IOS開發之旅-KVO

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

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

/* 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定義以下:less

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參數指向同一個地址,進行額外參數的傳遞。ide

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

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

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

/* 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參數的值spa

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方法?觀察者必須實現以下方法code

/* 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

或者orm

+(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的簡單總結,若有不正確的地方,請給我留言。

相關文章
相關標籤/搜索