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

プレゼンテーション


概要

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

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

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

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

利点

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

欠点

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

問題となる箇所

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

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

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

チュートリアル

プロジェクトの作成

モデルの作成

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

Personクラス

- (Person *)copy;
- (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を用いて行います。

@interface ObjectIDCopyAndPaste_AppDelegate : NSObject 
{
	.
	.
	.
	IBOutlet NSArrayController *arrayController;
	NSArray *_cachedObjects;
}
- (IBAction)copy:(id)sender;
- (IBAction)paste:(id)sender;
#import "Person.h"
NSString *PersonPBoardType = @"PersonPBoardType";
- (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];
}

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

- (void)pasteboardChangedOwner:(NSPasteboard *)sender
{
	[_cachedObjects autorelease];
}
- (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];
    }
}

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

テスト

テスト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による方法を併用する方法も考えられます。

関連

Core Data

Core DataでのCopy & Paste (Property list編)

リファレンス

Copying and Copy and Paste

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

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