KVOとは
KVOとはキー値監視(Key-Value Observing)のことを指します。
アプリ開発をかじったことのある人ならMVCという言葉を聞いたことがあると思いますが、KVOはModelの変更をControllerやViewに通知し、反映させるための仕組みです。
他の通知システムとしてはdelegateやNSNotificationを利用した方法などがあります。通知システムの使い分けについてはここでは扱いません。(詳しく知りたい方はこちらなど)
作ったアプリ
メモを追加するだけの簡単なアプリです。
メモにはタイトルとメモの内容を書くことができ、テーブルビューで表示されます。
AutoLayoutも使ってみたかったので、テキストの長さに合わせてセルのサイズが変わるようになっています。
作ったコードはgithubにもあげているので参考にしてください。(こちら)
アプリの構成
.
├── AppDelegate.h
├── AppDelegate.m
├── Models
│   ├── MemoManager.h
│   ├── MemoManager.m
│   └── Data Models
│       ├── Memo.h
│       └── Memo.m
├── Views
│   ├── AddModalView.h
│   ├── AddModalView.m
│   └── Custom Cells
│       ├── MemoCell.h
│       └── MemoCell.m   
├── Controllers
│   ├── ViewController.h
│   ├── ViewController.m
│   └── ViewController.xib
└── main.m
ホーム画面
追加画面
KVOの実装
本アプリではModelでメモが追加されたときにViewを更新する処理を行います。
したがって、メモが追加されたタイミングでModelから通知を送り、ビューの更新作業をおこなう処理を実装します。
KVOを使用するために複雑な実装は必要なく、以下の数ステップで完了することができます。
Controllerの実装
KVOの登録
まず、KVOによる通知を受け取るためにControllerで監視することを宣言します。viewDidLoad内などで以下のように実装することで変更を監視することができます。
今回はMemoManagerがメモを格納する配列memosを持っているので、memosを監視するようにしました。
- (void)viewDidLoad {
    [super viewDidLoad];
    
    // KVO で `MemoManager` の `memos` を監視する.
    [[MemoManager sharedManager] addObserver:self
                                          forKeyPath:@"memos"
                                             options:NSKeyValueObservingOptionNew
                                             context:nil];
    
    /** 略 */
}
通知を受け取る
次に通知を受け取る処理を実装します。KVOによる監視では、Modelが変更されたときの通知をobserveValueForKeyPathメソッドで受け取ります。変更を受け取ったControllerはViewの更新処理を行います。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if(object == [MemoManager sharedManager] && [keyPath isEqualToString:@"memos"]){
        // 配列が変更された場所のインデックス
        NSIndexSet *indexSet = change[NSKeyValueChangeIndexesKey];
        
        // 変更の種類.
        NSKeyValueChange changeKind = (NSKeyValueChange)[change[NSKeyValueChangeKindKey] integerValue];
        
        // 配列に詰め替え.
        NSMutableArray *indexPaths = [NSMutableArray array];
        [indexSet enumerateIndexesUsingBlock:^(NSUInteger index, BOOL *stop) {
            [indexPaths addObject:[NSIndexPath indexPathForRow:index inSection:0]];
        }];
        
        // `memos` の変更の種類に合わせて TableView を更新.
        [self.tableView beginUpdates]; // 更新開始.
        if (changeKind == NSKeyValueChangeInsertion) {
            // 新しく追加されたとき.
            [self.tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationAutomatic];
        }
        else if (changeKind == NSKeyValueChangeRemoval) {
            // 取り除かれたとき.
            [self.tableView deleteRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationAutomatic];
        }
        else if (changeKind == NSKeyValueChangeReplacement) {
            // 値が更新されたとき.
            [self.tableView reloadRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationAutomatic];
        }
        [self.tableView endUpdates]; // 更新終了.
    }
}
Modelの実装
KVOによる通知を送信するためには、それようのメソッドを用いる必要があります。配列に追加する場合は、mutableArrayValueForKeyを使います。
- (void)addMemo:(Memo *)memo withBlock:(void (^)(BOOL))block
{   
    // KVO 発火のため `mutableArrayValueForKey:` を介して insert する.
    [[self mutableArrayValueForKey:@"memos"] insertObjects:@[memo]
                                                 atIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, 1)]];
}
さいごに
KVOの実装は非常に簡単におこなうことができました。
まだ良さを完璧には理解できていないのですが、TableViewやCollectionViewのときに変更のあった場所だけ更新することができたり、バッチ処理のような多くのオブジェクトを一気に保存するときに逐一ビューに反映させることができたりするのかなーと思ってます。
何か間違っている点や、KVOの良さを教えてくださる方がいればコメントお待ちしてます!
参考
以下のサイトやコードを参考に作成しました。
Apple キー値コーディング プログラミングガイド
MVCもやもや話
[Objective-C] KeyValueObserve(キー値監視)メモ
hatena/ios-Intern-Bookmark-2013



