ファイルをNSViewにドラッグしてパスを取得する (Objective-C)


# 概要
- `NSView`のカスタムクラスを作成し、ファイルをドラッグ・アンド・ドロップをしたときにファイルのパスを取得できるようにする実装を行う。

![May-23-2019 12-59-00](https://i.imgur.com/S8iBcju.gif)

# GitHub

https://github.com/pommdau/Qiita_DragAndDrop_CustomView

# カスタムクラスの実装
- `DragAndDropView`という名称でNSViewを継承したCocoaClassファイルを作成する。

## AppDelegate

コード全体は以下の通り。

``` ObjectiveC
#import "AppDelegate.h"
#import "DragAndDropView.h"

@interface AppDelegate () <DragAndDropViewDelegate>
@property (weak) IBOutlet DragAndDropView *dragAndDropView;
@property (weak) IBOutlet NSTextField *resultTextField;

@property (weak) IBOutlet NSWindow *window;
@end

@implementation AppDelegate

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
    // Insert code here to initialize your application
    _dragAndDropView.delegate = self;
}


- (void)applicationWillTerminate:(NSNotification *)aNotification {
    // Insert code here to tear down your application
}

// MARK;- DragAndDropViewDelegate  Methods
- (void)DragAndDropViewGetDraggingFiles:(NSArray *)files {
    NSMutableString *resultMessage = [NSMutableString string];
    for (NSString *file in files) {
        [resultMessage appendFormat:@"%@\r\n", file];
    }
    [_resultTextField setStringValue:resultMessage];
}

// MARK:- Button Action
- (IBAction)clear:(id)sender {
    [_resultTextField setStringValue:@""];
}

@end
```

## DragAndDropView.h

コード全体は以下の通り

``` ObjectiveC
#import <Cocoa/Cocoa.h>
@protocol DragAndDropViewDelegate <NSObject>
- (void)DragAndDropViewGetDraggingFiles:(NSArray *)files;  // ビュー上にファイルがD&Dされた場合に呼ばれる
@end

@interface DragAndDropView : NSView
@property (weak, nonatomic) id <DragAndDropViewDelegate> delegate;
@end
```

- プロトコルとメソッド引数にファイルのパスを指定したもの）を定義する
- これを、元のViewを管理するコントローラ（= delegate先）呼び出す。

## DragAndDropView.m

コード全体は以下の通り。

``` ObjectiveC
#import "DragAndDropView.h"

@interface DragAndDropView ()
@property (nonatomic) BOOL highlight;   // View上にファイルがドラッグされているならば、ハイライトをつける
@end

@implementation DragAndDropView

/**
 @brief IB上でオブジェクトが生成される際に呼ばれる
 */
- (id)initWithCoder:(NSCoder *)decoder {
    self = [super initWithCoder:decoder];
    if (self) {
        [self setHighlight:NO];
        [self registerForDraggedTypes:[NSArray arrayWithObject:NSFilenamesPboardType]];
    }
    return self;
}

/**
 @brief Viewの描画処理
        View上にファイルがドラッグされているならば、ハイライトをつける
 */
- (void)drawRect:(NSRect)rect{
    [super drawRect:rect];
    if (_highlight) {
        [[NSColor systemBlueColor] set];
        [NSBezierPath setDefaultLineWidth: 5];
        [NSBezierPath strokeRect: [self bounds]];
    } else {
        [[NSColor grayColor] set];
        [NSBezierPath setDefaultLineWidth: 1];
        [NSBezierPath strokeRect: [self bounds]];
    }
}

// MARK:- NSDraggingDestination Protocol Methods

/**
 @brief Viewの境界にファイルがドラッグされるときに呼ばれる
        宛先がどのドラッグ操作を実行するのかを示す値を返す必要があります。
 */
- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender{
    [self setHighlight:YES];
    [self setNeedsDisplay: YES];    // ハイライトの情報が変更された場合に再描画を行う
    return NSDragOperationGeneric;
}


/**
 @brief View上にファイルがドラッグで保持されている間、短い間隔毎に呼ばれるメソッド
 宛先がどのドラッグ操作を実行するのかを示す値を返す必要があります。
 */
- (NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender{
    [self setHighlight:YES];
    [self setNeedsDisplay: YES];
    return NSDragOperationGeneric;
}

/**
 @brief View上にファイルがドラッグされなくなった際に呼ばれる
 */
- (void)draggingExited:(id <NSDraggingInfo>)sender{
    [self setHighlight:NO];
    [self setNeedsDisplay: YES];
}


/**
 @brief View上でファイルがドロップされた際に呼ばれる
 メッセージが返された場合はYES、performDragOperation:メッセージが送信されます。
 */
- (BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender {
    [self setHighlight:NO];
    [self setNeedsDisplay: YES];
    return YES;
}


/**
 @brief View上でファイルがドロップされた後の処理
 */
- (BOOL)performDragOperation:(id < NSDraggingInfo >)sender {
    NSArray *draggedFilenames = [[sender draggingPasteboard] propertyListForType:NSFilenamesPboardType];
    /*  // 対応していないファイルがドロップした場合の処理の例
    if ([[[draggedFilenames objectAtIndex:0] pathExtension] isEqual:@"txt"]){
        return YES; // テキストファイルのみ対象とする
    } else {
        return NO;
    }
     */
    for (NSString *draggedFilename in draggedFilenames) {
        BOOL isDir = NO;
        if ([[NSFileManager defaultManager] fileExistsAtPath:draggedFilename isDirectory:&isDir] == NO) {
            return NO;
        }
        if (isDir == YES) {
            return NO;      // フォルダがあった場合はエラーとする
        }
    }
    return YES;
}

/**
 @brief 一連のドラッグ操作が完了したときに呼ばれる
 */
- (void)concludeDragOperation:(id <NSDraggingInfo>)sender{
    NSArray *filePaths = [[sender draggingPasteboard] propertyListForType:NSFilenamesPboardType];
    [_delegate DragAndDropViewGetDraggingFiles:filePaths];
}
@end
```


``` ObjectiveC
#import "DragAndDropView.h"

@interface DragAndDropView ()
@property (nonatomic) BOOL highlight;   // View上にファイルがドラッグされているならば、ハイライトをつける
@end
```

- ハイライトを描画するかどうかのプロパティを定義しておく

``` ObjectiveC
@implementation DragAndDropView

/**
 @brief IB上でオブジェクトが生成される際に呼ばれる
 */
- (id)initWithCoder:(NSCoder *)decoder {
    self = [super initWithCoder:decoder];
    if (self) {
        [self setHighlight:NO];
        [self registerForDraggedTypes:[NSArray arrayWithObject:NSFilenamesPboardType]];
    }
    return self;
}
```

- `initWithCoder`はInterface Builder上でこのクラスのオブジェクトが作成される際に呼ばれる。
    - [awakeFromNibとinitWithFrameとinitWithCoderの使い方](https://qiita.com/reikubonaga/items/cb52dea471cfe1640773)

> * initWithFrameは、code上でobjectを作る時に呼ばれる
> * initWithCoderは、interface builder(storyboardやnibファイルなど)からobjectを作るときに呼ばれる

- `registerForDraggedTypes`はドラッグアンドドロップを受け付けるものを指定する。今回はファイルのURL

``` ObjectiveC
/**
 @brief Viewの描画処理
        View上にファイルがドラッグされているならば、ハイライトをつける
 */
- (void)drawRect:(NSRect)rect{
    [super drawRect:rect];
    if (_highlight) {
        [[NSColor systemBlueColor] set];
        [NSBezierPath setDefaultLineWidth: 5];
        [NSBezierPath strokeRect: [self bounds]];
    } else {
        [[NSColor grayColor] set];
        [NSBezierPath setDefaultLineWidth: 1];
        [NSBezierPath strokeRect: [self bounds]];
    }
}
```

- カスタムビューの周りに線を引くことで、ハイライトのための描画処理を行っている

## NSViewのドラッグ時の処理
### 境界にファイルがドラッグされたとき

``` ObjectiveC
// MARK:- NSDraggingDestination Protocol Methods

/**
 @brief Viewの境界にファイルがドラッグされるときに呼ばれる
        宛先がどのドラッグ操作を実行するのかを示す値を返す必要があります。
 */
- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender{
    [self setHighlight:YES];
    [self setNeedsDisplay: YES];    // ハイライトの情報が変更された場合に再描画を行う
    return NSDragOperationGeneric;
}
```

- `NSDragOperationGeneric`はここでは特に制限していないの意？
    - [（Apple公式）NSDragOperation](https://developer.apple.com/documentation/appkit/nsdragoperation?language=objc)

> NSDragOperationGeneric
> The operation can be defined by the destination.

### ビュー上にファイルがドラッグされているとき

``` ObjectiveC
/**
 @brief View上にファイルがドラッグで保持されている間、短い間隔毎に呼ばれるメソッド
 宛先がどのドラッグ操作を実行するのかを示す値を返す必要があります。
 */
- (NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender{
    [self setHighlight:YES];
    [self setNeedsDisplay: YES];
    return NSDragOperationGeneric;
}
```

### ファイルがドロップされなくなったとき
 
``` ObjectiveC
/**
 @brief View上にファイルがドラッグされなくなった際に呼ばれる
 */
- (void)draggingExited:(id <NSDraggingInfo>)sender{
    [self setHighlight:NO];
    [self setNeedsDisplay: YES];
}
```

### ファイルがドロップされたとき
 
``` ObjectiveC
/**
 @brief View上でファイルがドロップされた際に呼ばれる
 メッセージが返された場合はYES、performDragOperation:メッセージが送信されます。
 */
- (BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender {
    [self setHighlight:NO];
    [self setNeedsDisplay: YES];
    return YES;
}
```

### ファイルがドロップされたとき
 
``` ObjectiveC
/**
 @brief View上でファイルがドロップされた後の処理
 */
- (BOOL)performDragOperation:(id < NSDraggingInfo >)sender {
    NSArray *draggedFilenames = [[sender draggingPasteboard] propertyListForType:NSFilenamesPboardType];
    /*  // 対応していないファイルがドロップした場合の処理の例
    if ([[[draggedFilenames objectAtIndex:0] pathExtension] isEqual:@"txt"]){
        return YES; // テキストファイルのみ対象とする
    } else {
        return NO;
    }
     */
    for (NSString *draggedFilename in draggedFilenames) {
        BOOL isDir = NO;
        if ([[NSFileManager defaultManager] fileExistsAtPath:draggedFilename isDirectory:&isDir] == NO) {
            return NO;
        }
        if (isDir == YES) {
            return NO;      // フォルダがあった場合はエラーとする
        }
    }
    return YES;
}
```

- 対応していないファイルが有った場合は、上記コメントアウトしているような処理でNOを返す。
- 今回はフォルダ（.appを含む）が合った場合にはエラーとしてNOを返すようにしている

### ファイルがドロップされたとき
 
``` ObjectiveC
/**
 @brief 一連のドラッグ操作が完了したときに呼ばれる
 */
- (void)concludeDragOperation:(id <NSDraggingInfo>)sender{
    NSArray *filePaths = [[sender draggingPasteboard] propertyListForType:NSFilenamesPboardType];
    [_delegate DragAndDropViewGetDraggingFiles:filePaths];
}
@end
```

- 全てのドラッグに関する処理が終わったら、ドラッグされたファイルのパスを引数にし、プロトコルで定義したメソッドをdelegate先で呼び出す

# 参考
- [Drag and Drop text file on MacOS X application](http://gazapps.com/wp/2011/08/01/drag-and-drop-text-file-on-macos-x-application/)
    - コードは主にここを参考に書いた
- [MacアプリTunacan開発の技術的メモ](https://qiita.com/croquette0212/items/b0679b28c8658fcebfa1)
    - 公式リファレンスを噛み砕いて説明してくれているので、メソッドの意味や役割がわかりやすい
- [（Apple公式）Introduction to Drag and Drop](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/DragandDrop/DragandDrop.html#//apple_ref/doc/uid/10000069i)
- [OSX ドラッグアンドドロップの調査](http://nakajijapan.github.io/blog/2015/01/20/osx-%E3%83%89%E3%83%A9%E3%83%83%E3%82%B0%E3%82%A2%E3%83%B3%E3%83%89%E3%83%89%E3%83%AD%E3%83%83%E3%83%97%E3%81%AE%E8%AA%BF%E6%9F%BB/)