ドラッグ&ドロップ検証用アプリケーション
実際に操作しながらドラッグ&ドロップの説明を読んだほうがわかりやすいのではないかと思って、テーブルビューにorderを表示した検証用アプリケーションを作成しました。下記リンクでダウンロードできます。
サンプルの書類も一緒に入れてあります。
新規ファイル
Xcodeのファイルメニューから「新規ファイル…」を実行します。CocoaグループのObjective-C classを選んで「次へ」をクリックします。

DragDropDataSourceと入力して完了です。

インターフェイスファイルを編集する
選択されているオブジェクトを知る必要があるので、配列コントローラのアウトレットを追加します。またリサージュ図形がドラッグコピーされたとき、コピーされたオブジェクトが挿入ポイントに正しく入る様にするために挿入される行を覚えておく必要がでてきます。そのためのインスタンス変数insertionRowを追加します。
//
// DragDropDataSource.h
// RepeatingMotifGenerator
//
// Copyright NovemberKou 2008 . All rights reserved.
//
#import <Cocoa/Cocoa.h>
@interface DragDropDataSource : NSObject
{
IBOutlet NSArrayController *arrayController;
int insertionRow;
}
@end
マクロ定義と文字列定数の宣言
またドラッグされたオブジェクトの行番号情報をペーストボードに書き込む際のデータタイプを決めておく必要があります。これを文字列定数として定義します。文字列定数をそれぞれの場所で直接使ってしまうと、タイプミスした場合に発見が困難なバグとなってしまうので、このようにグローバル変数を使うのがよく使われるテクニックの様です。変数ならタイプミスした時にコンパイラが教えてくれます。
//
// DragDropDataSource.m
// RepeatingMotifGenerator
//
// Copyright NovemberKou 2008 . All rights reserved.
//
#import "DragDropDataSource.h"
#import "Lissajous.h"
#undef UNDO_MANAGER
#define UNDO_MANAGER [[arrayController managedObjectContext] undoManager]
// Pboard type
NSString *RMGRowsType = @"RMGRowIndexesPboardType";
@implementation DragDropDataSource
管理対象オブジェクトが追加、削除された時に順番を振り直す
RMGRootエンティティのorder属性はデフォルト値が-1になっているので、追加されたLissajousクラスのオブジェクトは皆orderが-1になっています。これを追加された時に適切に書き換えたいのですが、さてどうしたらよいでしょうか?
素直に考えると、管理対象オブジェクトのawakeFromInsertメソッドで現在のオブジェクト数を数え、それプラス1にorderを設定するとなると思うのですが、これをやると問題が発生します。これは下の失敗例を見て下さい。
問題がないのは管理対象オブジェクトコンテキストが発行する通知を受け取って処理する方法です。NSManagedObjectContextObjectsDidChangeNotificationという恐ろしく長い名前の通知を受け取ります。Didと名前がついている通り、変更処理が終わってから送られてくるので、awakeFromInsertの様な問題が発生しません。
別のドキュメントの管理対象オブジェクトコンテキストが発行する通知を受け取ってしまわない様に、通知を受け取る登録をする際に、objectとして配列コントローラと結びつけられた管理対象オブジェクトコンテキストを指定します。配列コントローラのアウトレットが有効である必要があるので、initメソッドではなくawakeFromNibメソッドで登録を実行します。
登録したら解除が必要です。deallocメソッドで登録解除をします。
//
// DragDropDataSource.m
//
- (void)awakeFromNib
{
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
insertionRow = -1;
[nc addObserver:self
selector:@selector(managedObjectChanged:)
name:NSManagedObjectContextObjectsDidChangeNotification
object:[arrayController managedObjectContext]];
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
[super dealloc];
}
【失敗例】awakeFromInsertでfetchを実行すると同じオブジェクトが二つ現れる
別のソフトウェアを開発している時に経験したのですが、awakeFromInsertでfetchを実行すると、そのオブジェクトが二つ現れるという問題が発生します。同じ問題に直面した人は他にもいるようで、検索したら以下のページが見つかりました。
Core Data : calling validation in awakeFromInsert problem ?
Insert処理が完了する前にfetchを実行するので、こんな不具合が発生するのだそうです。遅延実行すれば解決するそうですが、これだと二回アンドゥしないと元に戻らない不具合が新たに発生するような気もします。
通知を受け取って順番を振り直す
managedObjectChanged:で追加された管理対象オブジェクトのorderを適切に設定します。これは説明が長くなるので、別のページで改めて説明します。
データソースとして必要なメソッドを実装する
ここからはNSTableDataSourceプロトコルで規定されているメソッドを実装していきます。まずはnumberOfRowsInTableView:ですが、これはゼロを返すとバインディングを使ってくれるそうです。AppleのサンプルプログラムDragAppにそう書いてありました。DragAppは/Developer/Examples/CoreDataの中に入っています。
同様にtableView:objectValueForTableColumn:row:もnilを返すとバインディングを使ってくれるそうです。
//
// DragDropDataSource.m
//
#pragma mark -
#pragma mark Data Source method
// ゼロを返すとテーブルビューは後戻りしてバインディングからデータを得る
- (int)numberOfRowsInTableView:(NSTableView *)tableView
{
return 0;
}
// nilを返すとテーブルビューは後戻りしてバインディングからデータを得る
- (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn
row:(int)rowIndex
{
return nil;
}
オブジェクトをペーストボードへ書き出す
このメソッドはドラッグを開始すべき時にテーブルビューから呼び出されます。ただしまだ実際にドラッグは開始されていませんので、このメソッドでNOを返せばドラッグは中止されます。YESを返してペーストボードにデータを置けばドラッグが始まります。ドラッグに使われる画像や、その他必要な情報は、YESを返せばテーブルビューが用意してくれます。
ペーストボードに置くデータは二つあります。ドラッグされたリサージュ図形が自分のテーブルビューにドロップされたときのためのデータと、他のドキュメントウィンドウのテーブルビューにドロップされた時のためのデータの二種類です。
他のドキュメントウィンドウのテーブルビューにドロップする時のために、ペーストボードにドラッグされているリサージュ図形をコピーします。ここで一つ失敗しているのですが、これは下の失敗例を見て下さい。
続いて自分自身にドロップされた時のために行番号情報をペーストボードにセットします。これは単に引数のrowIndexesをアーカイブしたデータです。
//
// DragDropDataSource.m
//
- (BOOL)tableView:(NSTableView *)tableView writeRowsWithIndexes:(NSIndexSet *)rowIndexes
toPasteboard:(NSPasteboard *)pboard
{
[Lissajous copyObjects:[[arrayController arrangedObjects] objectsAtIndexes:rowIndexes]
toPasteboard:pboard];
[pboard addTypes:[NSArray arrayWithObject:RMGRowsType] owner:nil];
[pboard setData:[NSArchiver archivedDataWithRootObject:rowIndexes]
forType:RMGRowsType];
return YES;
}
【失敗例】コピーするオブジェクトを間違えた
オブジェクトをペーストボードにコピーするコードですが、最初は
[Lissajous copyObjects:[arrayController selectedObjects]
toPasteboard:pboard];
と書いていました。選択されているリサージュ図形をコピーしていたわけです。
しばらく不具合に気づかなかったのですが、たまたま選択されているのと違うオブジェクトをドラッグしてコピーしてみた時に間違いに気づきました。ドラッグしたリサージュ図形ではなく、そのとき選択されていたリサージュ図形がコピーされてしまったからです。
あらゆる操作を想定して試験するというのは難しいですね。
ドラッグ操作を検証する
tableView:validateDrop:ではドラッグ元とドラッグ先が同じ場合はNSDragOperationMove、違う場合はNSDragOperationCopyを返しています。ただしオプションキーが押されていたらNSDragOperationCopyを返します。
最後にドラッグ元のマスクと論理積を取って返り値とします。もしもドラッグ元がコピーを禁止していれば、dragOperationがNSDragOperationCopyであったとしても、ここでNSDragOperationNoneになるわけです。
tableView:validateDrop:proposedRow:proposedDropOperation:メソッドは基本的にtableView:validateDrop:の値を返すだけなのですが、引数のoperationを使って一仕事させています。
引数のoperationは行の上にドロップしようとしているのか(NSTableViewDropOn)、行の間にドロップしようとしているのか(NSTableViewDropAbove)を表します。行の上にドロップしようとしている場合は、setDropRow:dropOperation:を使って行の間にドロップする動作に変更するという事をやっています。
//
// DragDropDataSource.m
//
- (NSDragOperation)tableView:(NSTableView *)tableView
validateDrop:(id <NSDraggingInfo>)info
{
NSDragOperation dragOperation;
dragOperation = ([info draggingSource] != tableView) ? NSDragOperationCopy : NSDragOperationMove;
if([[NSApp currentEvent] modifierFlags] & NSAlternateKeyMask)
dragOperation = NSDragOperationCopy;
return dragOperation & [info draggingSourceOperationMask];
}
- (NSDragOperation)tableView:(NSTableView *)tableView
validateDrop:(id <NSDraggingInfo>)info
proposedRow:(int)row
proposedDropOperation:(NSTableViewDropOperation)operation
{
if(operation == NSTableViewDropOn)
[tableView setDropRow:row dropOperation:NSTableViewDropAbove];
return [self tableView:tableView validateDrop:info];
}
ドロップ操作を受け入れる
最後にtableView:acceptDrop:row:dropOperation:メソッドですが、これは説明が長くなりますので、次のページにまわします。とりあえずリストだけ掲載しておきます。
//
// DragDropDataSource.m
//
- (void)rearrangeObjects
{
[[UNDO_MANAGER prepareWithInvocationTarget:self] rearrangeObjects];
[arrayController rearrangeObjects];
}
- (BOOL)tableView:(NSTableView*)tableView
acceptDrop:(id <NSDraggingInfo>)info
row:(int)row
dropOperation:(NSTableViewDropOperation)operation
{
NSPasteboard *pboard = [info draggingPasteboard];
NSDragOperation dragOperation = [self tableView:tableView validateDrop:info];
if(dragOperation == NSDragOperationCopy)
{
insertionRow = row;
[Lissajous pasteFromPasteboard:pboard controller:arrayController];
[UNDO_MANAGER setActionName:NSLocalizedString(@"dragCopyLissajous",nil)];
return YES;
}
else if(dragOperation == NSDragOperationMove)
{
int i,order,index,first,last;
NSIndexSet *rowIndexes;
RMGRoot *object;
NSArray *arrangedObjects = [arrayController arrangedObjects];
NSEnumerator *enumerator = [arrangedObjects objectEnumerator];
NSMutableArray *orders = [NSMutableArray arrayWithCapacity:[arrangedObjects count]];
while(object = [enumerator nextObject])
[orders addObject:[object order]];
rowIndexes = [NSUnarchiver unarchiveObjectWithData:[pboard dataForType:RMGRowsType]];
// 挿入された行までのドラッグされていないオブジェクトの番号を振り直す
first = row < [rowIndexes firstIndex] ? row : [rowIndexes firstIndex];
if(first == 0)
order = 0;
else
order = [[[arrangedObjects objectAtIndex:first-1] order] intValue] + 1;
for(i=first;i<row;i++)
{
if([rowIndexes containsIndex:i]) continue;
object = [arrangedObjects objectAtIndex:i];
[object setValue:[orders objectAtIndex:order++] forKey:@"order"];
}
// ドラッグされているオブジェクトの番号を振り直す
index = [rowIndexes firstIndex];
while(index != NSNotFound)
{
object = [arrangedObjects objectAtIndex:index];
[object setValue:[orders objectAtIndex:order++] forKey:@"order"];
index = [rowIndexes indexGreaterThanIndex:index];
}
// 挿入された行以降のドラッグされていないオブジェクトの番号を振り直す
last = row > [rowIndexes lastIndex] ? row : [rowIndexes lastIndex];
for(i=row;i<last;i++)
{
if([rowIndexes containsIndex:i]) continue;
object = [arrangedObjects objectAtIndex:i];
[object setValue:[orders objectAtIndex:order++] forKey:@"order"];
}
[self rearrangeObjects];
[UNDO_MANAGER setActionName:NSLocalizedString(@"dragOperation",nil)];
return YES;
}
else
return NO;
}

ホーム
前へ