プロキシ(NSProxy)による変更通知 (その2)

また日が空いてしまったが更に続きを。

//この後やること

//プロパティ名から現在のプロパティ値取得

//メソッドのパラメタから新たに設定されるはずのプロパティ値を取得

//新旧のプロパティを比較して変更されていれば通知

ここを埋めていく。通知するのは後でデリゲートに渡すのもよし、セレクタを登録するも良しだが、今回は話を単純にするために、特定のインタフェース(カテゴリ)に沿ったメソッドを実行することにする。

NotifyPropertyChangeカテゴリ(NSObject+NotifyPropertyChange.h)
#import <Foundation/Foundation.h>

@interface NSObject (NotifyPropertyChange)
- (void)notifyWillPropertyChange: (NSString *) propertyName;
- (void)notifyDidPropertyChanged: (NSString *) propertyName;
@end

このようなカテゴリは「非形式プロトコル」と呼ばれ、どんなクラスでも拡張することができる。※

これ自体、パラメタにはプロパティ名を単に通知をするだけで何も役に立たないが、検証には十分だ。
実装はこうなる。

NotifyChangeProxy-forwardInvocation:
- (void)forwardInvocation:(NSInvocation *)invocation
{
    if ( targetObject )
    {   
        NSString *methodName = NSStringFromSelector([invocation selector]);
        
        if ([methodName hasPrefix:@"set"]) //setプロパティ名 アクセッサか? 
        {
            int len = [methodName length]-3;
            NSString *subs = [methodName substringWithRange:NSMakeRange(3, len-1)];
            NSString *propertyName = [subs stringByReplacingCharactersInRange:NSMakeRange(0,1) withString:[[subs substringToIndex:1] lowercaseString]];
            SEL getter = NSSelectorFromString(propertyName);
            if ([targetObject respondsToSelector:getter])
            {
                //現在のプロパティ値を取得
                id oldVal = [targetObject performSelector:getter];
                
                id newVal;
                [invocation getArgument:&newVal atIndex:3];
                if ( newVal && ![oldVal isEqual:newVal] ) //現在値と新しい値が違うか? 
                {
                    //プロパティ変更前を通知
                    if ([targetObject respondsToSelector:@selector(notifyWillPropertyChange:)])
                    {
                        [targetObject notifyWillPropertyChange:propertyName];
                    }
                    //プロパティ変更後を通知
                    if ([targetObject respondsToSelector:@selector(notifyDidPropertyChanged:)])
                    {
                        [invocation setTarget:targetObject];
                        [invocation invoke];
                        [targetObject notifyDidPropertyChanged:propertyName];
                        return;
                    }
                }
            }
        }
        [invocation setTarget:targetObject];
        [invocation invoke];
    }
}

ポイントとしては、プロパティ値のセット前と後の値を比較して、変わるようであれば通知対象ということで、対象のオブジェクトがカテゴリに反応してくれるかを調べるためにrespondsToSelector:メッセージを送って検査している。YESが返ればそれぞれnotifyWillPropertyChange:、notifyDidPropertyChanged:メッセージが送れるという訳だ。

これで.NET C#のINotifyPropertyChangedよろしくオブジェクトの変更通知を自動的に行うための実装ができた。
しかし、Mac OS Xのプログラミングに詳しい方はとっくにご存じだと思うが、この「プロパティの変更通知機能」はCocoaフレームワークとして既に実装されているのだ。

それが Key-Value Observing 略して KVOである。
Key-Value Observing Programming Guide: Introduction to Key-Value Observing Programming Guide

次回からはこのKVOを使った変更通知とiOSでの活用を考えていきたいと考えている。

※カテゴリによる非形式プロトコルは継承に頼らずソースコードすらないクラスを拡張することが出来る、私が考えるにObjective-Cで最も緩く最も素晴らしい機能だ。