2010年3月アーカイブ

2010年3月27日

NSViewのbindingには既にfont周りの実装が入っているっぽいぞ。

NSUserDefaultsってNSFontとかNSColorを保存するときってNSArchiver/NSUnarchiver通してNSDataにしてから保存するじゃないですか。まあ、何となく使ってると不便だけど何となくそのしきたりに慣れちゃってそこそこ何となくいい感じになっていってしまうって感じなんですが。

        NSFont* font = [NSFont messageFontOfSize:12];
        NSData* data = [[NSUserDefaults standardUserDefaults] dataForKey:@"defaultFont"];
        if (data) {
            font = (NSFont*)[NSUnarchiver unarchiveObjectWithData:data];
        }

こんな風にですね。

で、最近もう随分とCocoa Bindingsにご厄介になりっぱなしなものだから、NSUserDefaultsの内容もこいつBindingを使って、PreferencesパネルでUserDefaultsを変更したら勝手に追従してくれるようなNSViewがあったらいいじゃんとかぐーたらなこと考えついたわけですよ。

	[myView bind:@"font" toObject:[NSUserDefaultsController sharedUserDefaultsController] withKeyPath:@"values.defaultFont" options:nil];

...

もちろんmyViewのクラスはNSViewから継承した上でこうしておいたわけです。

- (void)bind:(NSString*)binding toObject:(id)observable withKeyPath:(NSString*)keyPath options:(NSDictionary*)options
{
    [super bind:binding toObject:observable withKeyPath:keyPath options:options];

    if ([binding isEqualToString:@"font"]) {
        _fontController = observable;
        [_fontKey release];
        _fontKey = [keyPath copy];
        
        [_fontController addObserver:self forKeyPath:keyPath options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:FontContext];
    }
}

- (void)observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void*)context
{
    NSLog(@"change = %@", [change description]);
    if (_fontController == object && [keyPath isEqual:_fontKey]) {
        [self setFont:[object valueForKey:keyPath]];
        
        [self reloadData];
    }
}

unbind:ももちろん実装したけどひとまず割愛。でもって動かしてみたらこうなった。

2010-03-27 01:34:59.954 HogeApp[34125:a0f] Cannot create NSFont from object <040b7374 7265616d 74797065 6481e803 84014084 8484064e 53466f6e 741e8484 084e534f 626a6563 74008584 01692484 055b3336 635d0600 00001a00 0000fffe 4c007500 63006900 64006100 47007200 61006e00 64006500 00008401 660d8401 63009801 98009800 86> of class NSCFData

ああ、そりゃそうだ。setFont:メソッドはこのmyViewのクラスでNSFontのインスタンスを扱うようにしてあるもの。でもNSUserDefaults/NSUserDefaultsControllerから取り出したのがNSDataのインスタンスであっても、setterメソッドくらいは呼ばれるだろ、っと思ってトレースしていっても、どうにも押さえられない。それどころかbind:ofObject:withKeyPath:options:メソッド中でも[super bind:...]した後のfontプロパティ用に独自実装した部分にすらやってこずにエラーになってて、あれーおかしいなって首ひねってたら、どうも面白いことがわかった。

- (void)testExposedBindings
{
	NSView *view = [[HogeView alloc] initWithFrame:NSZeroRect];
	NSLog(@"HogeView exposedBindings = %@", [view exposedBindings]);
	[view release];
}

HogeViewはこうしておく。

@interface HogeView : NSView {
    NSFont* font;
};

@property(retain,nonatomic) NSFont* font;

@end

@implementation HogeView

@synthesize font;

@end

このHogeViewに、fontプロパティを(setter/getterメソッドを実装するか、@propertyを使うか)実装した状態で実行したときと、無い場合(@property行と@synthesize行をコメントアウト)では、自動的にexposedBindingsの結果が違ってるのね。

2010-03-27 02:03:34.658 Intermezzo[34475:a0f] HogeView exposedBindings = (
    fontFamilyName,
    fontSize,
    fontBold,
    hidden,
    fontName,
    font,
    toolTip,
    fontItalic
)
2010-03-27 02:03:50.261 Intermezzo[34505:a0f] HogeView exposedBindings = (
    toolTip,
    hidden
)

前者がfontプロパティ有りのとき、後者が無しのとき。どうもリフレクションか何かが働いてbindingのexposeをよしなにやってくれるらしい。これはちょっとびっくりした。そうか。bind:...メソッドを継承して独自実装とかってのはそもそも要らんわけね。

しかしこのままだとbind:toObject:withKeyPath:options:メソッドが使えない。このメソッドの中でNSUserDefaultsから取り出したNSDataオブジェクトを勝手にNSFontだと勘違いして処理を進めようとしてエラー吐いてるらしいってことなので、何とかしてこれを解決させなければならない。...とここまで来て忘れてましたよ。NSValueTranslatorの存在に。NSDataとNSFontの間を変換してくれるトランスレータを書いてあっさりと解決。


@interface CZFontDataTransformer : NSValueTransformer {
}

+ (Class)transformedValueClass;
+ (BOOL)allowsReverseTransformation;

@end

@implementation CZFontDataTransformer

+ (Class)transformedValueClass
{
	return [NSFont class];
}

+ (BOOL)allowsReverseTransformation
{
	return YES;
}

- (id)transformedValue:(id)value
{
	if ([value isKindOfClass:[NSData class]]) {
		return [NSUnarchiver unarchiveObjectWithData:value];
	}
	return nil;
}

- (id)reverseTransformedValue:(id)value
{
	if ([value isKindOfClass:[NSFont class]]) {
		return [NSArchiver archivedDataWithRootObject:value];
	}
	return nil;
}

@end

もちろんbind時のオプションに指定すんですよ。

	[logView bind:@"font" toObject:[NSUserDefaultsController sharedUserDefaultsController] withKeyPath:@"values.defaultFont" options:
	 [NSDictionary dictionaryWithObject:[[[CZFontDataTransformer alloc] init] autorelease] forKey:NSValueTransformerBindingOption]];

font以外にも同じように面倒見てくれるbindingがあるかどうかは続報を待て。