データソースの汎用性を高める【実践的Macintoshプログラミング解説】

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

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

LinkIconホーム

更新日 2009-05-24

汎用性を高める工夫

データソースの汎用性を高める

 モデルオブジェクトのクラス名を直接ソースコードに記述しない事でNVKTableViewの汎用性を高める事ができました。同じ事をDragDropDataSourceクラスに適用すれば、同様に汎用性を高める事ができるので実行します。

 クラス名をNVKDragDropDataSourceとします。また爆発アニメーションのデリゲートメソッドを追加します。

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

 インターフェイスファイルの変更点はクラス名のみです。

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

#import <Cocoa/Cocoa.h>


@interface NVKDragDropDataSource : NSObject
{
    IBOutlet    NSArrayController   *arrayController;

    int         insertionRow;
    NSIndexSet  *selectionIndexes,*oldSelectionIndexes;
}

@end

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

 DragDropDataSourceとの違いを赤字で示しながら説明していきます。

  • Lissajous.hをインポートしていた部分がNVKPasteboardSupportプロトコルを定義するヘッダファイルに置き換わりました。モデルクラスはこのプロトコルを採用している事が前提となります。
  • Lissajous.h経由でRMGRoot.hをインポートしなくなったので、UNDO_MANAGERマクロの定義を取り消す必要がなくなりました。よって#undef文を削除しています。
  • 管理対象オブジェクトコンテキストへのアクセスを容易にするためにMOCマクロを追加しました。
  • 行番号情報のデータタイプを表す文字列とグローバル変数の接頭詞をRMGからNVKに変更しました。
  • 配列コントローラに設定されているエンティティ名に対応するクラスのクラスオブジェクトを返すtargetClassメソッドを追加しました。これはNVKTableViewと同じメソッドです。

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

#import "NVKDragDropDataSource.h"

#import "NVKPasteboardSupport.h"

#define     MOC             [arrayController managedObjectContext]
#define     UNDO_MANAGER    [[arrayController managedObjectContext] undoManager]

// Pboard type
NSString    *NVKRowsType = @"NVKRowIndexesPboardType";

@implementation NVKDragDropDataSource

//  配列コントローラに設定されているエンティティ名に対応するクラスのクラスオブジェクトを返す
- (Class)targetClass
{
    NSEntityDescription *entity = [NSEntityDescription entityForName:[arrayController entityName]
                                              inManagedObjectContext:MOC];

    return  NSClassFromString([entity managedObjectClassName]);
}

初期化メソッド、アクセサメソッド、ユーティリティメソッド

 awakeFromNibメソッドにターゲットとなるモデルクラスがNVKPasteboardSupportを採用しているかどうかをチェックするコードを追加しました。していない場合はエラーメッセージを出力するので、デバッグの際に役立つでしょう。

 deallocメソッドは変更なしです。アクセサメソッドとユーティリティメソッドも変更ありません。

//
//  NVKDragDropDataSource.m
//

#pragma mark -
#pragma mark Initializer and Deallocator

- (void)awakeFromNib
{
    NSNotificationCenter    *nc = [NSNotificationCenter defaultCenter];

    insertionRow = -1;
    [nc addObserver:self
           selector:@selector(managedObjectChanged:)
               name:NSManagedObjectContextObjectsDidChangeNotification
             object:MOC];

    selectionIndexes = oldSelectionIndexes = nil;
    [self bind:@"selectionIndexes"
      toObject:arrayController
   withKeyPath:@"selectionIndexes"
       options:nil];

    //  debug support
    if(![[self targetClass] conformsToProtocol:@protocol(NVKPasteboardSupport)])
        NSLog(@"[NVKDragDropDataSource]:Class '%@' does not adopt NVKPasteboardSupport!",[self targetClass]);
}

- (void)dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    [selectionIndexes release];
    [oldSelectionIndexes release];
    [super dealloc];
}

#pragma mark -
#pragma mark Accessor methods

- (NSIndexSet *)selectionIndexes    {return  selectionIndexes;}
- (void)setSelectionIndexes:(NSIndexSet *)newSelection
{
    if(![UNDO_MANAGER isUndoing] && ![UNDO_MANAGER isRedoing])
    {
        [newSelection retain];
        [oldSelectionIndexes release];
        oldSelectionIndexes = selectionIndexes;
        selectionIndexes = newSelection;
    }
}

#pragma mark -
#pragma mark Utility methods

- (void)setActionName:(NSString *)actionName
{
    if([UNDO_MANAGER isUndoing] || [UNDO_MANAGER isRedoing])
        return;
    
    if(![[UNDO_MANAGER undoActionName] isEqualToString:@""])
        return;
    
    [UNDO_MANAGER setActionName:NSLocalizedString(actionName,nil)];
}

管理対象オブジェクトが追加、削除された時の並べ替え

 通知を受けて管理対象オブジェクトの順番を並べ直す際の処理ですが、これまでは順序を表す属性を@"order"と固定の文字列で直接指定していました。これをクラスメソッドorderKeyを使って得た文字列を使う様に変更します。

 また追加と削除のアクション名をターゲットクラスのクラスメソッドを使って得る様に変更します。

//
//  NVKDragDropDataSource.m
//

#pragma mark -
#pragma mark Check Managed Objects

- (void)managedObjectChanged:(NSNotification *)notification
{
    int                 order = 0;
    NSSet               *insertedObjects,*deletedObjects;
    NSEnumerator        *enumerator;
    NSManagedObject     *object;
    NSMutableArray      *insertedTargetObjects;
    NSString            *orderKey = [[self targetClass] orderKey];

    insertedObjects = [[notification userInfo] objectForKey:NSInsertedObjectsKey];
    deletedObjects = [[notification userInfo] objectForKey:NSDeletedObjectsKey];

    if([insertedObjects count] > 0 && ![UNDO_MANAGER isUndoing] && ![UNDO_MANAGER isRedoing])
    {
        insertedTargetObjects = [NSMutableArray arrayWithCapacity:[insertedObjects count]];

        //  配列コントローラが管理するオブジェクトの中から挿入されたオブジェクトを抜き出す
        enumerator = [[arrayController arrangedObjects] objectEnumerator];
        while(object = [enumerator nextObject])
            if([insertedObjects containsObject:object])
                [insertedTargetObjects addObject:object];
        if(insertionRow < 0)
            insertionRow = [[arrayController arrangedObjects] count] - [insertedTargetObjects count];

        //  挿入されたオブジェクト以外のオブジェクトのorderを設定する
        order = 0;
        enumerator = [[arrayController arrangedObjects] objectEnumerator];
        while(object = [enumerator nextObject])
        {
            if(order == insertionRow)
                order += [insertedTargetObjects count];
            if(![insertedTargetObjects containsObject:object])
                [object setValue:[NSNumber numberWithInt:order++] forKey:orderKey];
        }

        //  挿入されたオブジェクトのorderを設定する
        order = insertionRow;
        enumerator = [insertedTargetObjects objectEnumerator];
        while(object = [enumerator nextObject])
            [object setValue:[NSNumber numberWithInt:order++] forKey:orderKey];
        insertionRow = -1;

        [arrayController rearrangeObjects];
    }
    if([insertedObjects count] > 0)
    {
        [arrayController setSelectedObjects:[insertedObjects allObjects]];
        [self setActionName:[[self targetClass] addActionName]];
    }
    if([deletedObjects count] > 0)
    {
        if([UNDO_MANAGER isUndoing])
            [arrayController setSelectionIndexes:oldSelectionIndexes];
        [self setActionName:[[self targetClass] deleteActionName]];
    }
}

データソースのメソッド

 以下の点を変更しています。

  • Lissajousと直接クラス名を記述していた部分を[self targetClass]に変更した。
  • RMGRowsTypeをNVKRowsTypeに変更した。
  • アクション名を固定文字列からターゲットクラスのクラスメソッドに変更した。
  • @"order"をターゲットクラスのorderKeyメソッドに変更した。
  • orderアクセサメソッドの呼び出しをvalueForKey:orderKeyに変更した。

//
//  NVKDragDropDataSource.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;  
}

- (BOOL)tableView:(NSTableView *)tableView writeRowsWithIndexes:(NSIndexSet *)rowIndexes
     toPasteboard:(NSPasteboard *)pboard
{
    [[self targetClass] copyObjects:[[arrayController arrangedObjects] objectsAtIndexes:rowIndexes]
                       toPasteboard:pboard];
    [pboard addTypes:[NSArray arrayWithObject:NVKRowsType] owner:nil];
    [pboard setData:[NSArchiver archivedDataWithRootObject:rowIndexes]
            forType:NVKRowsType];

    return  YES;
}

- (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];
}

- (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;
        [[self targetClass] pasteFromPasteboard:pboard controller:arrayController];
        [UNDO_MANAGER setActionName:NSLocalizedString([[self targetClass] dragCopyActionName],nil)];
        return  YES;
    }
    if(dragOperation == NSDragOperationMove)
    {
        int             i,order,index,first,last;
        id              object;
        NSIndexSet      *rowIndexes;
        NSArray         *arrangedObjects = [arrayController arrangedObjects];
        NSEnumerator    *enumerator = [arrangedObjects objectEnumerator];
        NSMutableArray  *orders = [NSMutableArray arrayWithCapacity:[arrangedObjects count]];
        NSString        *orderKey = [[self targetClass] orderKey];

        while(object = [enumerator nextObject])
            [orders addObject:[object valueForKey:orderKey]];

        rowIndexes = [NSUnarchiver unarchiveObjectWithData:[pboard dataForType:NVKRowsType]];

        //  挿入された行までのドラッグされていないオブジェクトの番号を振り直す
        first = row < [rowIndexes firstIndex] ? row : [rowIndexes firstIndex];
        if(first == 0)
            order = 0;
        else
            order = [[[arrangedObjects objectAtIndex:first-1] valueForKey:orderKey] intValue] + 1;
        for(i=first;i<row;i++)
        {
            if([rowIndexes containsIndex:i])    continue;
            object = [arrangedObjects objectAtIndex:i];
            [object setValue:[orders objectAtIndex:order++] forKey:orderKey];
        }
        //  ドラッグされているオブジェクトの番号を振り直す
        index = [rowIndexes firstIndex];
        while(index != NSNotFound)
        {
            object = [arrangedObjects objectAtIndex:index];
            [object setValue:[orders objectAtIndex:order++] forKey:orderKey];
            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:orderKey];
        }

        [self rearrangeObjects];
        [UNDO_MANAGER setActionName:NSLocalizedString([[self targetClass] dragOperationActionName],nil)];
        return YES;
    }
    else
        return  NO;
}

爆発アニメーション終了時の削除処理

 テーブルビューのアイテムがドラッグされてドロップを受け入れられない場所でリリースされた場合、爆発アニメーションが表示された後にデータソースにanimationEffectDidEnd:メッセージが送られてきます。データソースはドラッグされたアイテムを削除する処理を実行する必要があります。

 このメソッドでは以下の処理を行なっています。

  1. ドラッグ用ペーストボードから行番号情報を取り出す。
  2. 行番号情報からドラッグされた管理対象オブジェクトを特定する。
  3. 特定した管理対象オブジェクトを管理対象オブジェクトコンテキストから削除する。
  4. アンドゥマネージャにアクション名を設定する。
  5. 管理対象オブジェクトコンテキストに未処理のまま残っている変更点を更新する

 5を実行しないと削除されるのにかなり時間がかかります。この解決策がなかなかわかりませんでした。イベントループをまたいでしまうので、手動でprocessPendingChangesを呼ばないといけないという事なのでしょうか?

//
//  NVKDragDropDataSource.m
//

//  爆発アニメーション終了後にテーブルビューから呼び出される
- (void)animationEffectDidEnd:(void *)contextInfo
{
    NSPasteboard    *pboard = [NSPasteboard pasteboardWithName:NSDragPboard];
    NSManagedObject *draggedObject;
    NSEnumerator    *enumerator;
    NSIndexSet      *rowIndexes;
    NSArray         *draggedObjects;
    
    rowIndexes = [NSUnarchiver unarchiveObjectWithData:[pboard dataForType:NVKRowsType]];
    draggedObjects = [[arrayController arrangedObjects] objectsAtIndexes:rowIndexes];
    enumerator = [draggedObjects objectEnumerator];
    while(draggedObject = [enumerator nextObject])
        [MOC deleteObject:draggedObject];
    [self setActionName:[[self targetClass] deleteActionName]];
    
    //  これを実行しないとなかなか消えてくれない
    [MOC processPendingChanges];
}