RMGRootにユーティリティメソッドを追加する【実践的Macintoshプログラミング解説】

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

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

LinkIconホーム

更新日 2009-05-24

リサージュ図形をカット&ペーストできる様にする

RMGRootにユーティリティメソッドを追加する

 旧バージョンのカット&ペースト対応は、オブジェクトをNSCodingプロトコルに適合させて、NSKeyedArchiverでアーカイブ化したデータをペーストボードに配置するというやり方でした。

 CoreDataを使った場合でも同じやり方が通用するのかと思ったら、残念ながらそうではありません。NSManagedObjectはNSCodingプロトコル対応ではないのです。

 以下で紹介する方法はAppleの以下の情報を参考にしました。

インターフェイスファイルにメソッドの宣言を追加する

 RMGRoot.hにユーティリティメソッドの宣言を追加します。ついでにmanagedObjectContextの短縮形MOCのマクロも追加しておきます。

  • dataTypeは管理対象オブジェクトの情報をペーストボードに書き込む時に、データを識別するためにつける型名を返すメソッドです。カット&ペーストに対応するサブクラスは、固有の型名を返します。
  • copyKeysは管理対象オブジェクトを辞書として表現する時に含めるキーの名前を返すメソッドです。サブクラスでそれぞれの属性に対応するキーの名前を配列に入れて返します。
  • dictionaryRepresentationは管理対象オブジェクトの辞書表現を返すメソッドです。関連がなく属性だけを持つ管理対象オブジェクトであれば、RMGRootクラスで実装する内容だけで用が足ります。関連を持つ場合はサブクラスで独自に対応する必要があります。
  • stringDescriptionは管理対象オブジェクトの文字列表現を返すメソッドです。文字列データは汎用性があるので、他のアプリケーションにペーストできるメリットがあります。
  • copyObjects:toPasteboard:は引数の配列に入ったオブジェクトをペーストボードに書き出すメソッドです。引数の配列に入ったオブジェクトはdataType, dictionaryRepresentation, stringDescriptionメッセージに応答する必要があります。個々のインスタンスに働きかける機能ではなく、インスタンス自体の生成に関わる機能なので、クラスメソッドにしました。
  • pasteFromPasteboard:controller:はペーストボード上のデータを使って管理対象オブジェクトを生成し、値を設定します。controllerはオブジェクトの生成と設定、選択に必要です。これも個々のインスタンスに働きかける機能ではなく、インスタンス自体の生成に関わる機能なので、クラスメソッドにしました。

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

#import <CoreData/CoreData.h>


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

@interface RMGRoot :  NSManagedObject  
{
}

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

- (id)rmgValueForKey:(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

ユーティリティメソッドを実装する

 dataTypeメソッドは必要なサブクラスが固有のデータタイプを返せばよいので、ここでは単にnilを返す実装としておきます。

 copyKeyメソッドも同様に、サブクラスでそれぞれの属性に対応するキーの名前を配列に入れて返せばよいので、ここでは単にnilを返す実装としておきます。

 stringDescriptionも同様に、サブクラスでそれぞれの内容に応じた文字列を返せばよいので、ここでは単にnilを返す実装としておきます。

 dictionaryRepresentationは管理対象オブジェクトの辞書表現を返すメソッドです。これはdictionaryWithValuesForKeys:という非常に便利なメソッドがあるので、これを使えば以下の様に簡潔に記述できます。(これは上述のAppleのページに記載されているコードそのものです)

 copyKeysはクラスメソッドなので[self copyKeys]ではなく[[self class] copyKeys]としている点に注意。また具体的なクラス名を書かず[self class]としている点も重要です。これによりRMGRootクラスのインスタンスでdictionaryRepresentationメソッドを実行する時は[RMGRoot copyKeys]となりますが、OscillatorCDクラスのインスタンスでdictionaryRepresentationメソッドを実行すると[OscillatorCD copyKeys]となるので、サブクラスで実装したcopyKeysクラスメソッドが使われる事になります。

//
//  RMGRoot.m
//

#pragma mark -
#pragma mark PasteBoard methods

+ (NSString *)dataType          {return  nil;}
+ (NSArray *)copyKeys           {return  nil;}
- (NSString *)stringDescription {return  nil;}

- (NSDictionary *)dictionaryRepresentation
{
    return  [self dictionaryWithValuesForKeys:[[self class] copyKeys]];
}

オブジェクトをペーストボードにコピーする

 引数の配列に入ったオブジェクトを、引数のペーストボードに書き出すメソッドcopyObjects:toPasteboard:を実装します。引数の配列に入ったオブジェクトはdictionaryRepresentationとstringDescriptionメッセージに応答する必要があります。

 データタイプというのはペーストボード上に存在するデータの種類を見分けるための文字列です。汎用的なデータであればNSStringPBoardTypeの様に、システムが用意するデータタイプを使います。独自のデータの場合は、他と衝突しない(であろう)文字列を自分で決めて使う事になります。

処理の流れ

  1. まずオブジェクトの数の分の容量を確保した可変配列(NSMutableArray)を作ります。可変配列はオブジェクトの辞書表現を入れるものと、文字列表現を入れるものの二つを用意します。
  2. その後、NSEnumeratorを使って引数の配列に入った全てのオブジェクトにアクセスし、可変配列にオブジェクトの辞書表現と文字列表現を追加していきます。
  3. 準備ができたので引数のペーストボードにdeclareTypes:owner:メッセージを送って、これから書き出すデータのタイプを通知します。遅延書き出しはしないのでowner:にはnilを渡します。
  4. そしてsetString:forType:で文字列を書き出し、setData:forType:で辞書表現の入った可変配列をアーカイブ化したデータを書き出して終了です。

//
//  RMGRoot.m
//

+ (void)copyObjects:(NSArray *)objects
       toPasteboard:(NSPasteboard *)pboard
{
    RMGRoot         *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]];
}

オブジェクトをペーストする

 さてコピーしたオブジェクトはペーストできなければ意味がありません。次はペーストボード上のデータを元にして管理対象オブジェクトを生成するメソッドpasteFromPasteboard:controller:を実装します。

処理の流れ

  1. まず引数のペーストボードから、自分のデータタイプのデータを読み込みます。データがなければこの時点で終了します。(ユーザインターフェイス的には、そもそもそのような状況を作らない様にしておかないといけないのですが)
  2. 読み込んだデータを非アーカイブ化します。するとアーカイブ前の状態に戻るので、オブジェクトの辞書表現が入った可変配列が得られます。
  3. 得られた配列からNSEnumeratorを作って、配列内の全てのオブジェクトに働きかけます。配列の中に入っているのはオブジェクトの辞書表現なので、まず管理対象オブジェクトを生成し、生成したオブジェクトにsetValuesForKeysWithDictionary:メッセージを送って、辞書の内容をオブジェクトに反映させます。
  4. objectsという可変配列に生成したオブジェクトを入れておき、最後にarrayControllerに対してこれらのオブジェクトを選択する様にメッセージを送って終了です。

//
//  RMGRoot.m
//

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

    if(data == nil)     return;

    dictionarys = [NSUnarchiver unarchiveObjectWithData:data];
    enumerator = [dictionarys objectEnumerator];
    objects = [NSMutableArray arrayWithCapacity:[dictionarys count]];
    while(dictionary = [enumerator nextObject])
    {
        object = [NSEntityDescription insertNewObjectForEntityForName:[controller entityName]
                                               inManagedObjectContext:[controller managedObjectContext]];
        [object setValuesForKeysWithDictionary:dictionary];
        [objects addObject:object];
    }
    [controller performSelector:@selector(setSelectedObjects:)
                     withObject:objects
                     afterDelay:0];    
}

 色々ありましたが、肝心な部分はdictionaryWithValuesForKeys:でオブジェクトを辞書に変換し、setValuesForKeysWithDictionary:で辞書の中身をオブジェクトに反映させるというところです。

【失敗例】CoreDataでうまくいかない時は、時間差攻撃が有効?


 上記のソースコード中、最後の行でオブジェクトの選択を行なっていますが、最初は素直にこう書いていました。

    [arrayController setSelectedObjects:objects];

 ところがこれではペーストしたオブジェクトを選択してくれないのです。NSLogを使ってarrayControllerのcontentを見てみると、ペーストしたオブジェクトはこの時点ではまだcontentの中に入っていませんでした。なるほど、それでは選択しようとしても無理というものです。配列コントローラにバインドされた管理対象オブジェクトコンテキストの中に管理対象オブジェクトを生成しても、それが直ちに配列コントローラのコンテントに反映されるわけではない(タイミングがずれる)のでしょう。

 普通に実行するとうまくいかないが、performSelector:withObject:afterDelay:メソッドを使って遅延実行するとうまくいくというケースがCoreDataでは多い様に思います。プログラマが何かやったつもりになっていても、実はそれは直ちに実行されるわけではないという事なのでしょう。便利さと引き換えに、内部処理を覆い隠すヴェールが厚くなった様な気がします。

 便利な遅延実行ですが、デメリットもあります。イベントループをまたいでしまうので、アンドゥが分割されてしまうのです。ユーザが一回の操作で実行した事を、二回アンドゥしないと取り消せないのでは違和感があるので、そのような場合は採用しづらいものがあります。
 今回は、選択操作がアンドゥの対象ではないので問題ありません。