[iOS x Design Pattern] 觀察者模式 Observer Pattern ( II )

既上回介紹Notification與KVO後又發現了一個KVO的用法KVO Compliance

前言

事情是這樣的,在與Johnny分享KVO的pattern後,建議他可以用KVO觀察某個陣列的數量來被動更新畫面,這麼一來就可以不用每次都加一個物件就再多加一段updateView,當天晚上我就回家再實作了一次,發現竟然沒有接收到通知!於是我立馬展開調查(撞牆)

錯誤示範

@interface LYObserverViewController ()
@property (strong,nonatomic) NSMutableArray *dataSource;
@end

@implementation LYObserverViewController
- (void)viewDidLoad
{
    [super viewDidLoad];
    
    self.dataSource = [[NSMutableArray alloc] init];
    
    //觀察陣列dataSource的數量
    [self.dataSource addObserver:self forKeyPath:@"count"
                         options:NSKeyValueObservingOptionNew
                         context:@"dataSourceSizeChanged"];
    //陣列加入物件(應該會驅動observe)
    [self.dataSource addObject:@"first"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
    if (context == @"dataSourceSizeChanged") {
        //印出結果
        NSLog(@"%@", change);
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

顯然這其中一定有什麼誤會,不然沒道理不會動。

Key-Value Observing

後來查了蠻久都找不到原因observe NSMutableArray count,最後參考官方文件Key-Value Observing Programming Guide,文中指出只要用Key-value coding compliant方式實作的物件都會自動發出通知,並且提供兩種手動機制來觸發通知:

1. Automatic Change Notification (自動通知)

從下面這段程式碼可以大約猜出,如果要Observe NSMutableArray的話要透過物件,並且在透過mutableArrayValueForKey取得的實體才會支援自動通知。

// Call the accessor method.
[account setName:@"Savings"];
 
// Use setValue:forKey:.
[account setValue:@"Savings" forKey:@"name"];
 
// Use a key path, where 'account' is a kvc-compliant property of 'document'.
[document setValue:@"Savings" forKeyPath:@"account.name"];
 
// Use mutableArrayValueForKey: to retrieve a relationship proxy object.
Transaction *newTransaction = <#Create a new transaction for the account#>;
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject:newTransaction];

2. Manual Change Notification (手動通知)

willChangeValueForKey:didChangeValueForKey給埋在Setter裡面,就可以當做發出通知的訊號,當然,你也可以故意做成setA然後Notify B,這邊的key你是可以自已定的。或者你自己用一個覆合式的通知,完全自定:)

- (void)setOpeningBalance:(double)theBalance {
    if (theBalance != _openingBalance) {
        [self willChangeValueForKey:@"openingBalance"];
        _openingBalance = theBalance;
        [self didChangeValueForKey:@"openingBalance"];
    }
}

解決方法

只要從mutableArrayValueForKey取的實體,在對它觀察就可以囉。

@implementation LYObserverViewController
- (void)viewDidLoad
{
    [super viewDidLoad];    
    self.dataSource = [[NSMutableArray alloc] init];

    //觀察陣列dataSource的數量
    [self addObserver:self forKeyPath:@"dataSource"
                         options:NSKeyValueObservingOptionNew
                         context:@"dataSourceSizeChanged"];
                         
    //從mutableArrayValueForKey取的實體
    //陣列加入物件(應該會驅動observe)
    NSMutableArray *array = [self mutableArrayValueForKey:@"dataSource"];
    [array addObject:@"first"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
    if (context == @"dataSourceSizeChanged") {
        //印出結果
        NSLog(@"%@", change);
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

後記

其實一開始只是想學好iOS的MVC架構,並且將Observer Pattern運用在上面(iOS好像本來就該這麼做),卻意外學到了怎麼用手動觸發通知,也算是不錯的經驗:)