Core DataでのCopy & Paste (objectID編)

Core DataでのCopy & Paste (objectID編)

Core DataでのCopy & PasteをobjectIDを用いて実現する方法

プレゼンテーション


概要

Copy & Pasteの流れは、Copy操作でコピー元となるオブジェクトをNSStringやNSData等のシリアライズ可能な形式にし、NSPasteboardオブジェクトに引き渡します。Paste操作でそのデータからコピーオブジェクトを生成します。

Core Dataでは、コピーの対象となるオブジェクトはNSManagedObjectのインスタンスになります。NSManagedObjectには、Copyで用いるシリアライズ用のメソッドは用意されていないので、何らかの手段を用いて実装する必要があります。

手段として以下の方法が考えられます。

  • Property listを用いる方法
  • objectIDを用いる方法
  • Custom Classを用いる方法

ここでは、objectID (NSManagedObjectID)を用いる方法について記述しています。

利点

コピー元のオブジェクトをそのまま得る事が出来ます。リレーション先のオブジェクトにも簡単にアクセスが出来、deep copyも思いのままです。

欠点

NSManagedObjectIDはNSPersistentStoreCoordinatorと関連づけられているため、異なるアプリケーション、異なるドキュメント間へのpasteが出来ません。

問題となる箇所

NSManagedObjectIDは、NSPersistentStoreCoordinatorに最初に保存するまで、一時的なIDが割り当てられます。保存後は永続的なIDに置き換わります。

もし、一時的なIDの時にcopy操作し、paste操作する前に保存された場合、永続的なIDに変わってしまうため、コピー元のオブジェクトを得る事が出来なくなってしまいます。

一時的なIDから永続的なIDに変わってもコピー元のオブジェクトが得られる様に、Lazy Writingと呼ばれるNSPasteboardへのデータ渡しを遅延させる方法を用います。

チュートリアル

プロジェクトの作成

  • XCodeを起動します。
  • ファイルメニューから新規プロジェクトを選択します。
  • アシスタントのApplicationグループ内のCore Data Applicationを選択し、次へをクリックします。
  • プロジェクト名を入力し、完了をクリックします。(ここではObjectIDCopyAndPasteにしました)

モデルの作成

名前と住所を持つアドレスブック用のモデルを作成します。

  • グループとファイルのModelsフォルダー内のObjectIDCopyAndPaste_DataModel.xcdatamodelの編集画面を表示させます。
  • エンティティを追加し、名前をPersonにします。
  • 属性を追加し、名前をnameにし、タイプを文字列にします。
  • 属性を追加し、名前をaddressにし、タイプを文字列にします。

Personクラス

  • Personクラスのパース
    • Personエンティティを選択します。
    • ファイルメニューから新規ファイルを選択します。
    • アシスタントの設計グループから、管理オブジェクトクラスを選択し、次へをクリックします。
    • 適切な保存場所を指定し(通常はそのままでOK)次へボタンをクリックします。
  • 完了をクリックします。
  • Person.hの編集
    • -copy:メソッド定義をPerson.hに追加します。
- (Person *)copy;
  • Person.mの編集
    • -copy:メソッドをPerson.hに追加します。
- (Person *)copy
{
	NSManagedObjectContext *context = [self managedObjectContext];
	Person *person = [NSEntityDescription insertNewObjectForEntityForName:@"Person" inManagedObjectContext:context];
	id object;

	object = [self valueForKey:@"name"];
	object = (object == [NSNull null]) ? nil : object;
	[person setValue:object forKey:@"name"];

	object = [self valueForKey:@"address"];
	object = (object == [NSNull null]) ? nil : object;
	[person setValue:object forKey:@"address"];
	return person;
}

ObjectIDCopyAndPaste_AppDelegateクラス

ObjectIDCopyAndPaste_AppDelegateクラスが、メインウインドウdelegateになっているので、このクラスにcopy:、paste:メソッドを実装します。データの管理、表示はNSArrayControllerを用いて行います。

  • ObjectIDCopyAndPaste_AppDelegate.hの編集
@interface ObjectIDCopyAndPaste_AppDelegate : NSObject 
{
	.
	.
	.
	IBOutlet NSArrayController *arrayController;
	NSArray *_cachedObjects;
}
    • -copy:と-paste:メソッドの定義を追加します。
- (IBAction)copy:(id)sender;
- (IBAction)paste:(id)sender;
  • ObjectIDCopyAndPaste_AppDelegate.mの編集
    • Person.hのインクルード
#import "Person.h"
NSString *PersonPBoardType = @"PersonPBoardType";
    • -copy:の実装
      • Lazy Writingを用いるのでcopy操作では、扱うデータ形式をNSPasteboardに伝えるだけです。
      • データの提供を遅延させるので、paste操作が行われる時まで、コピー元のオブジェクトを_cachedObjectsに確保しておきます。
- (IBAction)copy:(id)sender
{
	NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
	NSArray *selectedObjects = [arrayController selectedObjects];

	NSArray *types = [NSArray arrayWithObjects:PersonPBoardType, NSStringPboardType, nil];
	[pasteboard declareTypes:types owner:self];
	_cachedObjects = [[NSArray alloc] initWithArray:selectedObjects];
}
  • [NSPasteboard declareTypes:owner:]でdelegateメソッド-pasteboardChangedOwner:が呼び出されます。

このタイミングで、前回のcopy操作でキャッシュしたコピー元のオブジェクトを解放します。

- (void)pasteboardChangedOwner:(NSPasteboard *)sender
{
	[_cachedObjects autorelease];
}
    • -paste:の実装
      • -paste:内の-[NSPasteboard dataForType:PersonPBoardType]でNSPasteboardからデータを取得しようとすると、delegateメッセージ-pasteboard:provideDataForType:が呼び出されます。-pasteboard:provideDataForType:でデータを提供します。
      • -pasteboard:provideDataForType:では-[NSManagedObject objectID]で得られたNSManagedObjectIDオブジェクトを-[NSManagedObjectID URIRepresentation]でシリアライズ可能なNSURLオブジェクトにした物を提供します。
      • -paste:内では、その逆の手順でNSManagedObjectIDを取り出し、-[NSManagedObjectContext objectWithID]でコピー元のオブジェクトを取得します。
      • -[Person copy]でコピーオブジェクトを生成し、NSArrayControllerに追加し、表示される様にします。
- (IBAction)paste:(id)sender
{
	NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
	NSString *availableType = [pasteboard availableTypeFromArray:[NSArray arrayWithObject:PersonPBoardType]];
	if ([availableType isEqualToString:PersonPBoardType]) {
		NSManagedObjectContext *context = [self managedObjectContext];
		NSPersistentStoreCoordinator *storeCoordinator = [context persistentStoreCoordinator];
		NSData *data = [pasteboard dataForType:PersonPBoardType];
		NSEnumerator *enumerator = [[NSUnarchiver unarchiveObjectWithData:data] objectEnumerator];
		NSURL *objectURI;
		while(objectURI = [enumerator nextObject]) {
			NSManagedObjectID *objectId = [storeCoordinator managedObjectIDForURIRepresentation:objectURI];
			id object = [context objectWithID:objectId];
			if (![object isDeleted]) {
				[arrayController addObject:[object copy]];
			}
		}
	}
}

- (void)pasteboard:(NSPasteboard *)sender provideDataForType:(NSString *)type
{
    if ([type isEqualToString:PersonPBoardType]) {
		NSEnumerator *enumerator = [_cachedObjects objectEnumerator];
		NSMutableArray *array = [NSMutableArray arrayWithCapacity:[_cachedObjects count]];
		Person *person;
		while(person = [enumerator nextObject]) {
			NSManagedObjectID *objectID = [person objectID];
			NSURL *objectURI = [objectID URIRepresentation];
			[array addObject:objectURI];
		}
		NSData *data = [NSArchiver archivedDataWithRootObject:array];
        [sender setData:data forType:PersonPBoardType];
    }
    else
	if ([type isEqualToString:NSStringPboardType]) {
		NSEnumerator *enumerator = [_cachedObjects objectEnumerator];
		Person *person;
		NSString *string = [NSString string];
		while(person = [enumerator nextObject]) {
			string = [NSString stringWithFormat:@"%@%@\n", string, [person name]];
		}
        [sender setString:string forType:NSStringPboardType];
    }
}

ユーザーインターフェイスの作成

  • XCodeのグループとファイルのResourcesフォルダ内のMainManu.nibをダブルクリックします。
  • XCodeでグループとファイルのObjectIDCopyAndPaste_AppDelegate.hをInterface Builder(以下IB)のMainManu.nibウインドウドラッグし、IB内のObjectIDCopyAndPaste_AppDelegateクラスのOutletとActionを更新します。
  • IBパレットからNSArrayControllerをMainManu.nibウインドウドラッグします。
  • ObjectIDCopyAndPaste_AppDelegateのOutlet arrayControllerをNSArrayControllerと接続します。
  • IBパレットからNSTableViewをWindowに追加します。
  • IBパレットからNSButtonを2つ追加します。
    • 1つはNSArrayControllerのadd:アクションに接続します。
    • もう1つはNSArrayControllerのremove:アクションに接続します。
    • ボタンの名称はそれぞれAdd、Deleteとします。
  • NSArrayControllerの設定
    • NSArrayControllerを選択し、cmd+1を押します。
    • ModelのRadioButtonでEntityを選択します。
    • Object Class NameをPersonにします。
    • Automatically prepares contextをチェックします。
  • Cocoa Binding設定。
    • cmd+4でBinding設定パネルを表示させます。
    • NSTableViewの設定
      • NSTableViewを選択し、cmd+1を押します。
      • 複数選択出来る様に、Multiple Selectionをチェックします。
    • NSArrayControllerの設定
      • NSArrayControllerを選択します。
      • NSManagedObjectContextのBind toをObjectIDCopyAndPaste_AppDelegateにし、Model Key PathをmanagedObjectContextに設定します。
    • NSTableViewColumn
      • NSTableViewの左のColumn(NSTableColumn)を選択します。
      • valueのBind toをNSArrayControllerにし、Controller KeyをarrangedObjects、Model Key Pathをnameにします。
      • NSTableViewの右のColumn(NSTableColumn)を選択します。
      • valueのBind toをNSArrayControllerにし、Controller KeyをarrangedObjects、Model Key Pathをaddressにします。

テスト

  • XCodeでビルドして実行させます。
  • テスト1
    • AddボタンでPersonオブジェクトを追加します。
    • name、addressを適当に入力します。
    • 行を選択し、Copy操作を行います。
    • Paste操作を行い、Copy & Pasteが正常に行われる事を確認します。
  • テスト2
    • AddボタンでPersonオブジェクトを追加します。
    • name、addressを適当に入力します。
    • 追加した行を選択し、Copy操作を行います。
    • cmd+sで保存します。
    • 一時的なIDの時にcopy操作を行い、保存後に永続的なID変わっても正常に行われる事を確認します。
  • テスト3
    • AddボタンでPersonオブジェクトを追加します。
    • name、addressを適当に入力します。
    • 追加した行を選択し、Copy操作を行います。
    • Paste操作を行い、Copy & Pasteが正常に行われる事を確認します。
    • cmd+sで保存します。
    • Paset操作を行います。しかし、今度はオブジェクは追加されましたが値は空文字になっていて、失敗してしまいました。

テスト3の対策

テスト2の手順では、Lazzy Writeを用いた事により、一時的なIDから永続的なIDに変わっても正常にCopy & Pasteが行われていますが、テスト3の手順を用いると、最初のPaste操作で一時的なIDとしてNSPasteboardにデータが提供され、その後保存してしまうと永続的なIDに変わっているため、問題となる箇所で述べた事と同じになってしまいます。

Paste操作で一時的なIDが含まれる場合は、保存によりIDが変わる可能性があります。そこでNSPasteboard上のデータを無効にし、次のPaste操作で再度データを要求する様にします。もし、永続的なIDに変わったとしても、その新しいIDでデータを提供する事が出来るので、うまくいきます。

-pasteboard:provideDataForType:を次の様に修正します。-[NSManagedObjectId isTemporaryID]で一時的なIDが含まれているか確認します。一時的なIDが含まれている場合は、-clearPersonPasteboard:を遅延呼び出しし、-clearPersonPasteboard:でNSPasteboardのPersonPBoardTypeデータをクリアーします。

遅延呼び出しが必要な理由は、-pasteboard:provideDataForType:はPaste処理中に呼び出され、NSPasteboardのデータを利用します。もし遅延呼び出ししないとNSPasteboardのデータがなくなってしまい、Paste処理がデータが提供出来なくなってしまうためです。

- (void)pasteboard:(NSPasteboard *)sender provideDataForType:(NSString *)type
{
    if ([type isEqualToString:PersonPBoardType]) {
		NSEnumerator *enumerator = [_cachedObjects objectEnumerator];
		NSMutableArray *array = [NSMutableArray arrayWithCapacity:[_cachedObjects count]];
		Person *person;
		BOOL needsInvalidation = NO;
		while(person = [enumerator nextObject]) {
			NSManagedObjectID *objectID = [person objectID];
			NSURL *objectURI = [objectID URIRepresentation];
			[array addObject:objectURI];

			if ([objectID isTemporaryID]) {
				needsInvalidation = YES;
			}
		}
		NSData *data = [NSArchiver archivedDataWithRootObject:array];
        [sender setData:data forType:PersonPBoardType];
		if (needsInvalidation) {
            [self performSelector:@selector(clearPersonPasteboard:) withObject:sender afterDelay:0];
		}
    }
    .
    .
    .

-clearPersonPasteboard:

- (void)clearPersonPasteboard:(NSPasteboard *)pb
{
    [pb addTypes:[NSArray arrayWithObject:PersonPBoardType] owner:self];
}

課題

一時的なIDのコピー元のオブジェクトが削除された場合は、コピー元のProperty値がnil等になっていて、正しく取得出来ません。そのため、Cut & Pasteする場合に支障が出ます*1。一時的なIDのコピー元のオブジェクトの削除前に、一旦保存*2してしまえば回避出来ますが、Document-Based Applicationの場合はCutと同時にダイアログが開いてしまう事もあり、インターフェイスとしてはあまり良くないと思います。

一時的なIDのコピー元のオブジェクトが削除される時に、Property listによる方法を併用する方法も考えられます。

*1:CutはCopy後、削除します

*2プログラムで保存させる