モデルクラスの対応【実践的Macintoshプログラミング解説】

印刷用表示 |テキストサイズ 小 |中 |大 |

CoreData版 Repeating Motif Generator の開発 Repeating Motif Wonderland CoreData 実践的 Macintosh プログラミング解説

LinkIconホーム

更新日 2009-05-24

汎用性を高める工夫

モデルクラスの対応

 モデルクラスのクラス名を直接書かない事で、NVKTableViewとNVKDragDropDataSourceの汎用性を高める事ができました。これらのクラスを使うのに必要な作業は以下の三つだけです。

  1. NVKTableViewにバインディングを設定する
  2. NVKTableViewのdataSourceアウトレットをNVKDragDropDataSourceのインスタンスに接続する
  3. NVKDragDropDataSourceインスタンスのarrayControllerアウトレットを接続する

 ソースコードを変更する事なく異なるモデルクラスに対応でき、カット&ペースト、ドラッグ&ドロップができる様になります。

 ただしこれはモデルクラスがNVKPasteboardSupportプロトコルを採用し、ペーストボード入出力に関するメソッドを実装している事が前提となります。そこでNVKPasteboardSupportプロトコルを採用し、ペーストボード入出力機能の実装を助けるルートクラスを用意する事にします。クラス名はRMGRootからNVKRootに変更しました。

NVKRootのインターフェイスファイル

 インターフェイスファイルの変更点は以下の通りです。追加・変更した点を赤字、削除した分を取り消し線で書いてあります。

 NVKPasteboardSupportプロトコルを採用したため、このプロトコルで定義しているメソッドの定義を削除しています。

 dictionaryRepresentationメソッドを強化して、サブクラスから呼び出す必要をなくしたため、宣言を削除しました。またcopyKeysメソッドを使う必要をなくしたので、宣言だけではなくメソッド自体を削除しています。

//
//  NVKRoot.h
//  RepeatingMotifGenerator
//
//  Copyright 2008 NovemberKou. All rights reserved.
//

#import <CoreData/CoreData.h>
#import "NVKPasteboardSupport.h"


#define     VFK(key)        [self nvkValueForKey:@key]
#define     MOC             [self managedObjectContext]
#define     UNDO_MANAGER    [[self managedObjectContext] undoManager]

@interface NVKRoot :  NSManagedObject  <NVKPasteboardSupport>
{
}

- (NSNumber *)order;
- (void)setOrder:(NSNumber *)value;

- (id)nvkValueForKey:(NSString *)key;
- (void)setValue:(id)value
          forKey:(NSString *)key
          action:(NSString *)actionName;

- (void)addXsObject:(id)value forKey:(NSString *)key;
- (void)removeXsObject:(id)value forKey:(NSString *)key;

+ (NSString *)dataType;
+ (NSArray *)copyKeys;
- (NSString *)stringDescription;
- (NSDictionary *)dictionaryRepresentation;

+ (void)copyObjects:(NSArray *)objects
       toPasteboard:(NSPasteboard *)pboard;
+ (void)pasteFromPasteboard:(NSPasteboard *)pboard
                 controller:(NSArrayController *)controller;

@end

インプリメンテーションファイルの変更点

 RMGRootとの違いを赤字で示しながら説明していきます。この辺りはRMG→NVKの機械的な置き換えしかありません。

// 
//  NVKRoot.m
//  RepeatingMotifGenerator
//
//  Copyright 2008 NovemberKou. All rights reserved.
//

#import "NVKRoot.h"


@implementation NVKRoot 

#pragma mark -
#pragma mark Methods for accessor

//  Getterを一行で書くためのメソッド
- (id)nvkValueForKey:(NSString *)key
{
    id  tmpValue;
    
    [self willAccessValueForKey:key];
    tmpValue = [self primitiveValueForKey:key];
    [self didAccessValueForKey:key];
    
    return tmpValue;
}

- (void)setActionName:(NSString *)actionName
{
    if(![UNDO_MANAGER isUndoing] && ![UNDO_MANAGER isRedoing])
        [UNDO_MANAGER setActionName:NSLocalizedString(actionName,nil)];
}

//  アンドゥのアクション名を同時にセットするSetter
- (void)setValue:(id)value
          forKey:(NSString *)key
          action:(NSString *)actionName
{
    [self willChangeValueForKey:key];
    [self setPrimitiveValue:value forKey:key];
    [self didChangeValueForKey:key];
    if(actionName != nil)
        [self setActionName:actionName];
}

//  対多関連のアクセサメソッド
- (void)addXsObject:(id)value forKey:(NSString *)key
{    
    NSSet *changedObjects = [[NSSet alloc] initWithObjects:&value count:1];
    
    [self willChangeValueForKey:key
                withSetMutation:NSKeyValueUnionSetMutation
                   usingObjects:changedObjects];
    [[self primitiveValueForKey:key] addObject:value];
    [self didChangeValueForKey:key
               withSetMutation:NSKeyValueUnionSetMutation
                  usingObjects:changedObjects];
    
    [changedObjects release];
}

- (void)removeXsObject:(id)value forKey:(NSString *)key
{
    NSSet *changedObjects = [[NSSet alloc] initWithObjects:&value count:1];
    
    [self willChangeValueForKey:key
                withSetMutation:NSKeyValueMinusSetMutation
                   usingObjects:changedObjects];
    [[self primitiveValueForKey:key] removeObject:value];
    [self didChangeValueForKey:key
               withSetMutation:NSKeyValueMinusSetMutation
                  usingObjects:changedObjects];
    
    [changedObjects release];
}

ペーストボード関連メソッド、アクセサメソッド、アクション名メソッド

 dictionaryRepresentationメソッドは大幅に変更しているので説明を後回しにして、それ以外の部分を先に説明します。

  • orderKeyメソッドはNVKPasteboardSupportプロトコルで定義しているメソッドです。キーの名前を@"order"とハードコーディングするのは好ましくないので、クラスメソッドにしました。
  • copyKeysメソッドはdictionaryRepresentationメソッドの変更に伴って不要となったので、削除しています。
  • NVKPasteboardSupportプロトコルで定義しているアクション名を返すメソッドを追加しています。ほとんどはサブクラスでオーバーライドすべきメソッドですが、ドラッグ操作はどのクラスでも共通でしょうからここで実装しています。

//
//  NVKRoot.m
//

#pragma mark -
#pragma mark PasteBoard methods

+ (NSString *)dataType          {return  nil;}
+ (NSString *)orderKey          {return  @"order";}
+ (NSArray *)copyKeys           {return  nil;}

+ (void)copyObjects:(NSArray *)objects
       toPasteboard:(NSPasteboard *)pboard
{
    NVKRoot         *object;
    NSMutableArray  *dictionarys,*strings;
    NSEnumerator    *enumerator = [objects objectEnumerator];

    dictionarys = [NSMutableArray arrayWithCapacity:[objects count]];
    strings = [NSMutableArray arrayWithCapacity:[objects count]];

    while(object = [enumerator nextObject])
    {
        [dictionarys addObject:[object dictionaryRepresentation]];
        [strings addObject:[object stringDescription]];
    }

    [pboard declareTypes:[NSArray arrayWithObjects:[[self class] dataType], NSStringPboardType, nil]
                   owner:nil];
    [pboard setString:[strings componentsJoinedByString:@"\n"]
              forType:NSStringPboardType];
    [pboard setData:[NSArchiver archivedDataWithRootObject:dictionarys]
            forType:[[self class] dataType]];
}

+ (void)pasteFromPasteboard:(NSPasteboard *)pboard
                 controller:(NSArrayController *)controller
{
    NSManagedObject     *object;
    NSArray             *dictionarys;
    NSDictionary        *dictionary;
    NSEnumerator        *enumerator;
    NSData              *data = [pboard dataForType:[[self class] dataType]];

    if(data == nil)     return;

    dictionarys = [NSUnarchiver unarchiveObjectWithData:data];
    enumerator = [dictionarys objectEnumerator];
    while(dictionary = [enumerator nextObject])
    {
        object = [NSEntityDescription insertNewObjectForEntityForName:[controller entityName]
                                               inManagedObjectContext:[controller managedObjectContext]];
        [object setValuesForKeysWithDictionary:dictionary];
    }
}

#pragma mark -
#pragma mark Accessor methods

- (NSNumber *)order                     {return  VFK("order");}
- (void)setOrder:(NSNumber *)value      {[self setValue:value forKey:@"order" action:nil];}

#pragma mark -
#pragma mark Action names

+ (NSString *)addActionName             {return  nil;}
+ (NSString *)deleteActionName          {return  nil;}
+ (NSString *)cutActionName             {return  nil;}
+ (NSString *)pasteActionName           {return  nil;}
+ (NSString *)dragCopyActionName        {return  nil;}
+ (NSString *)dragOperationActionName   {return  @"dragOperation";}

@end

オブジェクトの辞書表現を返す

 これまではオブジェクトの辞書表現に含める属性をcopyKeysメソッドで指定していました。この方法は以下の様に色々と問題があります。

  1. クラスごとにその都度判断して、ソースコードにキー文字列を入力するので手間がかかる。
  2. 対応できるのは属性だけで、関連は対応できない。
  3. 関連を辞書表現に含めたい場合は、各クラスでdictionaryRepresentationメソッドをオーバーライドする必要がある。
  4. キー文字列とアクセサメソッドを追加して対応しているので、対多関連に含まれるオブジェクトの数が可変になると対応できない。

 そこでdictionaryRepresentationメソッドを強化して、上記の問題解決を試みる事にしました。

問題点に対する解決策

 1の問題は、エンティティの情報からオブジェクトの辞書表現に含める属性と関連を決定する事で解決できます。全ての属性と関連を含めるのではなく、以下の条件で絞り込めば実用上問題ないのではないかと思います。

  • 属性は「一時」がチェックされているものを除く
    • 今回の場合でいえばベジエパスが該当します。他の属性がセットされた時に再構成されるので、辞書表現に含めなくても問題ありません。
    • 最終的にファイルに保存されない情報なので、重要ではないと考えてよいでしょう。
  • 関連は削除ルールが「カスケード」になっているものだけを辞書表現に含める
    • 削除ルールが「カスケード」になっているという事は、関連に設定されたオブジェクトを所有しているようなものです。その場合は辞書表現に含めて、コピーの対象とすべきです。逆にカスケードになっていない場合は、単に参照しているだけと考えられますので、辞書表現に含めてコピーの対象とすべきではありません。

 2と3の問題は上記対策で関連を辞書表現に含めているので、これで解決できます。

 4の問題はわざと違う属性名、あるいは関連名を辞書表現に含めて、setValue:forUndefinedKey:メソッドが呼ばれる様にする事で対策します。サブクラスはsetValue:forUndefinedKey:メソッドをオーバーライドし、引数のキー文字列を見て処理内容を決定します。これで関連の数だけアクセサメソッドを追加しなくてもよくなり、対多関連に含まれるオブジェクトの数が決まっていなくても対応できる様になります。

 違う名前で辞書表現に含めるといってもランダムな名前では判断できないので、正しい名前の頭にアンダーバーを付け足した文字列を使う事にします。

ソースコードの説明

 以上の対策を実装したのが以下のコードです。最初に属性、次に関連の順で処理しています。

 エンティティから属性を取り出すにはattributesByNameメソッドを使います。これで属性の名前がキーになった辞書を得る事ができますので、NSEnumeratorを使ってループを回しています。ループの中ではキー値コーディングを使って属性名から値を取り出し、辞書表現として返す辞書に追加しています。辞書にnilを登録する事はできませんので、nilの場合はNSNullオブジェクトに置き換えて辞書に登録しています。こうしておくと辞書からオブジェクトに戻す時にsetValuesForKeysWithDictionary:が使えて、再びnilに戻ります。

 エンティティから関連を取り出すにはrelationshipsByNameメソッドを使います。これで関連の名前がキーになった辞書を得る事ができますので、NSEnumeratorを使ってループを回しています。ループの中では対多関連かどうかで処理を分けています。

 対多関連の場合は更にループを回して全てのオブジェクトの辞書表現が入った辞書を作成し、それを辞書表現として返す辞書に追加しています。その際、対多関連に含まれる全オブジェクトのキーはorderを文字列に変換したものとしています※。また全オブジェクトの辞書表現が入った辞書のキーは、対多関連の名前の先頭にアンダーバーを追加したものとしています。例えば"mixers"関連は"_mixers"というキーで登録されるわけです。

 対一関連の場合はキー値コーディングを使って関連名から値を取り出し、辞書表現として返す辞書に追加しています。キーは関連の名前の先頭にアンダーバーを追加したものとしています。

※対多関連の場合はorderが適切に設定されている必要があるという事です。orderが全部デフォルトの-1では困ります。

stringDescriptionのデフォルト実装

 stringDescriptionはnilを返すのではなく、せっかく辞書表現があるのだからそれを文字列にしたもの(プロパティリスト)を返す事にしました。サブクラスでオーバーライドしない場合でも、それなりに役に立つのではないかと思います。

//
//  NVKRoot.m
//

- (NSDictionary *)dictionaryRepresentation
{
    NSMutableDictionary         *dict, *toManyDict;
    NSEnumerator                *enumerator, *toManyEnumerator;
    NSAttributeDescription      *attribute;
    NSRelationshipDescription   *relationship;
    id                          object, dictRep;

    dict = [NSMutableDictionary dictionaryWithCapacity:10];
    enumerator = [[[self entity] attributesByName] objectEnumerator];
    while(attribute = [enumerator nextObject])
    {
        object = [self valueForKey:[attribute name]];
        if(object == nil)   object = [NSNull null];

        if(![attribute isTransient])
            [dict setObject:object forKey:[attribute name]];
    }
    enumerator = [[[self entity] relationshipsByName] objectEnumerator];
    while(relationship = [enumerator nextObject])
    {
        if([relationship deleteRule] == NSCascadeDeleteRule)
        {
            if([relationship isToMany])
            {
                toManyEnumerator = [[self mutableSetValueForKey:[relationship name]] objectEnumerator];
                toManyDict = [NSMutableDictionary dictionaryWithCapacity:10];
                while(object = [toManyEnumerator nextObject])
                {
                    dictRep = [object dictionaryRepresentation];
                    if(dictRep == nil)  dictRep = [NSNull null];
                    [toManyDict setObject:dictRep
                                   forKey:[NSString stringWithFormat:@"%d",[[object order] intValue]]];
                }
                //  setValue:forUndefinedKey:を使うためにアンダーバーを付け足して違う名前にする
                [dict setObject:toManyDict forKey:[NSString stringWithFormat:@"_%@",[relationship name]]];
            }
            else
            {
                object = [self valueForKey:[relationship name]];
                if(object == nil)
                    dictRep = nil;
                else
                    dictRep = [object dictionaryRepresentation];
                if(dictRep == nil)  dictRep = [NSNull null];
                [dict setObject:dictRep forKey:[NSString stringWithFormat:@"_%@",[relationship name]]];
            }
        }
    }
    return  dict;
}

- (NSString *)stringDescription {return  [[self dictionaryRepresentation] description];}

辞書表現の実例

 実例があった方が理解しやすいと思いますので、上記のメソッドで作成した辞書がどうなっているのかお見せします。

 まだ説明していないのですが、ミキサーと発振器のorderは作成時に設定する様にしています。ミキサーは0と1、発振器は0から5を設定しています。

(
        {
        "_mixers" =         {
            0 =             {
                "_oscillators" =                 {
                    0 =                     {
                        amplitude = 100;
                        frequency = 1;
                        order = 0;
                        phaseLag = 0;
                    };
                    1 =                     {
                        amplitude = 25;
                        frequency = 4;
                        order = 1;
                        phaseLag = 224;
                    };
                    2 =                     {
                        amplitude = 10;
                        frequency = 7;
                        order = 2;
                        phaseLag = 290;
                    };
                };
                order = 0;
            };
            1 =             {
                "_oscillators" =                 {
                    3 =                     {
                        amplitude = 100;
                        frequency = 1;
                        order = 3;
                        phaseLag = 60;
                    };
                    4 =                     {
                        amplitude = 25;
                        frequency = 4;
                        order = 4;
                        phaseLag = 43;
                    };
                    5 =                     {
                        amplitude = 10;
                        frequency = 3;
                        order = 5;
                        phaseLag = 0;
                    };
                };
                order = 1;
            };
        };
        order = 0;
        resolution = 200;
    }
)

MasterMotifCDクラスの対応

 以上の変更を受けてサブクラスがどのように対応したらよいのかを説明します。

 まずMasterMotifCDクラスですが、copyKeysメソッドはもう不要になったので、削除します。またdictionaryRepresentationメソッドをオーバーライドする必要もなくなったので、削除します。dictionaryRepresentationメソッドの為に追加した各関連に対応するアクセサメソッドも不要なので削除します。

 追加するのはvalueForUndefinedKey:とsetValue:forUndefinedKey:メソッドです。valueForUndefinedKey:は"_mixers"で例外が発生しない様にするために、オーバーライドする事が必要になります。setValue:forUndefinedKey:メソッドでは、キーが"_mixers"であった場合に引数の辞書から"0"と"1"のキーで辞書を取り出して、その辞書を使って各ミキサーの値をセットします。

 以上の様に、非常にシンプルに実装できる様になりました。

//
// MasterMotifCD.m
//

#pragma mark -
#pragma mark PasteBoard methods

+ (NSString *)dataType      {return  @"RMGMasterMotifPBoardType";}

+ (NSArray *)copyKeys
{
    static NSArray *masterMotifCopyKeys = nil;

    if(masterMotifCopyKeys == nil)
        masterMotifCopyKeys = [[NSArray alloc] initWithObjects:@"resolution", @"order", nil];

    return  masterMotifCopyKeys;
}

- (NSString *)stringDescription
{
    return  [NSString stringWithFormat:@"%3d, %@, %@",
             [[self resolution] intValue],[[self x] stringDescription],[[self y] stringDescription]];
}

- (NSDictionary *)dictionaryRepresentation
{
    NSMutableDictionary *dictionary;

    dictionary = [[[super dictionaryRepresentation] mutableCopy] autorelease];
    [dictionary setObject:[[self x] dictionaryRepresentation] forKey:@"xmixer"];
    [dictionary setObject:[[self y] dictionaryRepresentation] forKey:@"ymixer"];

    return  dictionary;
}

- (NSDictionary *)xmixer    {return nil;}
- (NSDictionary *)ymixer    {return nil;}

- (void)setXmixer:(NSDictionary *)dictionary    {[[self x] setValuesForKeysWithDictionary:dictionary];}
- (void)setYmixer:(NSDictionary *)dictionary    {[[self y] setValuesForKeysWithDictionary:dictionary];}


- (id)valueForUndefinedKey:(NSString *)key
{
    if([key isEqualToString:@"_mixers"])
        return  nil;
    else
        return  [super valueForUndefinedKey:key];
}

- (void)setValue:(id)value forUndefinedKey:(NSString *)key
{
    if([key isEqualToString:@"_mixers"])
    {
        [[self x] setValuesForKeysWithDictionary:[value objectForKey:@"0"]];
        [[self y] setValuesForKeysWithDictionary:[value objectForKey:@"1"]];
        return;
    }
    else
        [super setValue:value forUndefinedKey:key];
}

MixerCDクラスの対応

 次にMixerCDクラスですが、基本的にMasterMotifCDクラスと同じです。

 copyKeysメソッドはもう不要になったので、削除します。またdictionaryRepresentationメソッドをオーバーライドする必要もなくなったので、削除します。dictionaryRepresentationメソッドの為に追加した各関連に対応するアクセサメソッドも不要なので削除します。

 追加するのはvalueForUndefinedKey:とsetValue:forUndefinedKey:メソッドです。valueForUndefinedKey:は"_oscillators"で例外が発生しない様にするために、オーバーライドする事が必要になります。setValue:forUndefinedKey:メソッドでは、キーが"_oscillators"であった場合に引数の辞書から"0"〜"5"のキーで辞書を取り出して、その辞書を使って各発振器の値をセットします。

//
// MixerCD.m
//

#pragma mark -
#pragma mark PasteBoard methods

+ (NSArray *)copyKeys
{
    return  nil;
}

- (NSString *)stringDescription
{
    return  [NSString stringWithFormat:@"%@, %@, %@",
             [[self osc1] stringDescription],[[self osc2] stringDescription],[[self osc3] stringDescription]];
}

- (NSDictionary *)dictionaryRepresentation
{
    NSMutableDictionary *dictionary;

    dictionary = [NSMutableDictionary dictionaryWithCapacity:3];
    [dictionary setObject:[[self osc1] dictionaryRepresentation] forKey:@"oscillator1"];
    [dictionary setObject:[[self osc2] dictionaryRepresentation] forKey:@"oscillator2"];
    [dictionary setObject:[[self osc3] dictionaryRepresentation] forKey:@"oscillator3"];

    return  dictionary;
}

- (NSDictionary *)oscillator1   {return  nil;}
- (NSDictionary *)oscillator2   {return  nil;}
- (NSDictionary *)oscillator3   {return  nil;}

- (void)setOscillator1:(NSDictionary *)dictionary   {[[self osc1] setValuesForKeysWithDictionary:dictionary];}
- (void)setOscillator2:(NSDictionary *)dictionary   {[[self osc2] setValuesForKeysWithDictionary:dictionary];}
- (void)setOscillator3:(NSDictionary *)dictionary   {[[self osc3] setValuesForKeysWithDictionary:dictionary];}

- (id)valueForUndefinedKey:(NSString *)key
{
    if([key isEqualToString:@"_oscillators"])
        return  nil;
    else
        return  [super valueForUndefinedKey:key];
}

- (void)setValue:(id)value forUndefinedKey:(NSString *)key
{
    if([key isEqualToString:@"_oscillators"])
    {
        switch([[self order] intValue])
        {
            case 0:
                [[self osc1] setValuesForKeysWithDictionary:[value objectForKey:@"0"]];
                [[self osc2] setValuesForKeysWithDictionary:[value objectForKey:@"1"]];
                [[self osc3] setValuesForKeysWithDictionary:[value objectForKey:@"2"]];
                break;
            case 1:
                [[self osc1] setValuesForKeysWithDictionary:[value objectForKey:@"3"]];
                [[self osc2] setValuesForKeysWithDictionary:[value objectForKey:@"4"]];
                [[self osc3] setValuesForKeysWithDictionary:[value objectForKey:@"5"]];
        }
    }
    else
        [super setValue:value forUndefinedKey:key];
}

OscillatorCDクラスの対応

 OscillatorCDクラスの対応は、copyKeysメソッドを削除するだけです。

PathHolderクラスの対応

 PathHolderクラスのスーパークラスをNVKRootに変更します。

//
//  PathHolder.h
//  RepeatingMotifGenerator
//
//  Copyright 2008 NovemberKou. All rights reserved.
//

#import <CoreData/CoreData.h>
#import "NVKRoot.h"


@interface PathHolder :  NVKRoot
{
}

- (void)updateBezierPath;

- (NSBezierPath *)bezierPath;
- (void)setBezierPath:(NSBezierPath *)value;

@end