at_yasu's blog

ロード的なことを

Dynamic Property

Pythonばっかいじってたせいで、dir(obj) とかやってしまいそうになりますが、ObjCではうまくいきません(当たりまえですが)。

でも、CoreData の Entity は上手く動いてます。そんなわけでできるはずなので探した結果、下記のようなコードになりましたとさ。

クラスのインスタンス変数を動的に取ってくる事が出来れば、一番いいのですが、さすがにそこまで書くのは今は面倒なので、プロパティ一覧は別に用意し、内部で読み込んでインスタンスメソッドを追加して使うという形にします。

プロパティ一覧は、「「KGDyProperty」を継承したクラス名.conf」というファイルを用意し、改行区切りでプロパティを書き込みます。そして、コンパイル時にそのファイルをリソースフォルダにコピーするようにしておきます。



KGDyProperty.h

//
//  KGDyProperty.h
//  kogetsu
//
//  Created by at_yasu on 10/11/26.
//  Copyright 2010 __MyCompanyName__. All rights reserved.
//

#import <Foundation/Foundation.h>

// ベースクラス
@interface KGDyProperty : NSObject {
}

@end


KGDyProperty.m

//
//  KGDyProperty.m
//  kogetsu
//
//  Created by at_yasu on 10/11/26.
//  Copyright 2010 __MyCompanyName__. All rights reserved.
//
// via http://efreedom.com/Question/1-3560364/Writing-Dynamic-Properties-Cocoa

#import <objc/objc-runtime.h>
#import "KGDyProperty.h"

// fn = obj.prop; の時に呼び出される。
id accessorGetter(id self, SEL _cmd)
{
    NSString *method = NSStringFromSelector(_cmd);
    // Return the value of whatever key based on the method name
    NSLog(@"%@ %@", self, NSStringFromSelector(_cmd));
    return nil;
}

// obj.prop = set の時に呼び出される。anID がプロパティ名になる
void accessorSetter(id self, SEL _cmd, NSObject* newValue)
{
    NSString *method = NSStringFromSelector(_cmd);
    
    // remove set
    NSString *anID = [[[method stringByReplacingCharactersInRange:NSMakeRange(0, 3) withString:@""] lowercaseString] stringByReplacingOccurrencesOfString:@":" withString:@""];
    
    // Set value of the key anID to newValue
    NSLog(@"%@ %@ id:%@", self, NSStringFromSelector(_cmd), anID);
}

@implementation KGDyProperty
- (id) init
{
    if ((self = [super init]))
    {}
    return self;
}

+ (id) allocWithZone:(NSZone *)zone
{
    
    if ((self = [super allocWithZone:zone]))
    {
        NSAutoreleasePool* rootPool = [[NSAutoreleasePool alloc] init];
        const char* name = class_getName([self class]);
        NSString* classname = nil;
        
        classname = [[NSString alloc] initWithCString:name
                                             encoding:NSUTF8StringEncoding];
        NSString* path = [[NSBundle mainBundle] pathForResource:classname
                                                         ofType:@"conf"];
        
        if (path)
        {
            NSArray* prop = nil;
            NSString* fileContents = nil;
            NSError* err = nil;
            
            fileContents = [[NSString alloc]
                            initWithContentsOfFile:path
                            encoding:NSUTF8StringEncoding
                            error:&err];
            
            if ([fileContents length])
            {
                prop = [fileContents componentsSeparatedByString:@"\n"];
            }
            else
            {
                NSLog(@"cannot load to %@: %@",path, err);
            }
            
            [fileContents release];
            
            // Add Property methods.
            for (NSString* name in prop)
            {
                NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
                if ([name length])
                {
                    NSString* _name = [name copy];
                    SEL getter = NSSelectorFromString(_name);
                    [[self class] resolveInstanceMethod:getter];
                    [_name release];
                    
                    _name = [[NSString alloc] initWithFormat:@"set%@",
                             [name capitalizedString], nil];
                    SEL setter = NSSelectorFromString(_name);
                    [[self class] resolveInstanceMethod:setter];
                    [_name release];
                }
                [pool release];
            }
        }
        else
        {
            NSLog(@"File<%@> is not found.", path);
        }
        
        [classname release];
        [rootPool release];
    }
    return self;
}

+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
    NSString *method = NSStringFromSelector(aSEL);
    
    if ([method hasPrefix:@"set"])
    {
        class_addMethod([self class], aSEL, (IMP) accessorSetter, "v@:@");
        return YES;
    }
    else
    {
        class_addMethod([self class], aSEL, (IMP) accessorGetter, "@@:");
        return YES;
    }
    return [super resolveInstanceMethod:aSEL];
}
@end

以下、任意の実装。

KGTwEntry.h

//
//  KGTwEntry.h
//  kogetsu
//
//  Created by at_yasu on 10/11/25.
//  Copyright 2010 __MyCompanyName__. All rights reserved.
//

#import <Foundation/Foundation.h>
#import "KGDyProperty.h"

@interface KGTwEntry: KGDyProperty {
@private
    NSNumber* favorited;
    NSDate* created_at;
    NSNumber* truncated;
}


@property (nonatomic, assign) NSNumber* favorited;
@property (nonatomic, retain) NSDate* created_at;
@property (nonatomic, assign) NSNumber* truncated;
@end

KGTwEntry.m

//
//  KGTwEntry.m
//  kogetsu
//
//  Created by at_yasu on 10/11/25.
//  Copyright 2010 __MyCompanyName__. All rights reserved.
//

#import "KGTwEntry.h"


@implementation KGTwEntry
@dynamic favorited;
@dynamic created_at;
@dynamic truncated;

@end

KGTwEntry.conf

favorited
created_at
truncated



そんな感じで、後はこんな感じのコードを書いて、

...
    KGTwEntry* ent = [[KGTwEntry alloc] init];
    ent.favorited = [NSNumber numberWithBool:YES];
    ent.created_at = [NSDate date];
    ent.truncated = [NSNumber numberWithBool:NO];
    [ent release];
...


実行すれば、

2010-11-26 02:05:30.746 testprop[1739:207] *nil description* setFavorited: id:favorited
2010-11-26 02:05:30.746 testprop[1739:207] *nil description* setCreated_at: id:created_at
2010-11-26 02:05:30.748 testprop[1739:207] *nil description* setTruncated: id:truncated

こんな表示が来ますよっと。