画像をNSViewにドラッグして情報を取得する (Swift)

# 追記(2022-02-09)
- [performDragOperation\(\_:\)](https://developer.apple.com/documentation/appkit/nsdraggingdestination/1415970-performdragoperation)はBool値を返すだけで、実際の処理は[concludeDragOperation\(\_:\)
](https://developer.apple.com/documentation/appkit/nsdraggingdestination/1416010-concludedragoperation)に書くのが良い。

>[concludeDragOperation\(\_:\)](https://developer.apple.com/documentation/appkit/nsdraggingdestination/1416010-concludedragoperation)
>For this method to be invoked, the previous performDragOperation(_:) must have returned true.

- 終了処理は[draggingEnded\(\_:\)](https://developer.apple.com/documentation/appkit/nsdraggingdestination/1416096-draggingended)に記載する

# 概要
- 以前に[ファイルをNSViewにドラッグしてパスを取得する \(Objective\-C\) \- Qiita](https://qiita.com/IKEH/items/904142af934a47b38f94)というObjective-Cの記事を書きました。
- こちらを`Swift`で書き直してみます。

![Aug-12-2020 22-11-12](https://i.imgur.com/FmIirMa.gif)

# GitHub
- https://github.com/pommdau/Qiita_DragAndDropViewDemo

# 参考
- [Drag and Drop Tutorial for macOS](https://www.raywenderlich.com/1016-drag-and-drop-tutorial-for-macos)
- [ファイルをNSViewにドラッグしてパスを取得する \(Objective\-C\) \- Qiita](https://qiita.com/IKEH/items/904142af934a47b38f94)
- [クリップボードから画像を取得する \- Qiita](https://qiita.com/IKEH/items/6c456a1e7730e5c4f68a)
    - Pasteboardのときと同様の実装を行っています
- [Mac や iOS でファイルの種類を表す識別子 Uniform Type Identifiers を拡張子から調べる\(Swiftで1行で出来る\)](https://niwatako.hatenablog.jp/entry/2016/02/23/140758)

# ドラッグ・アンド・ドロップを受け付けるNSViewのカスタムクラスの作成
- はじめに、呼び出し元にドラッグされた画像の情報を返すため、プロトコルのメソッドとして宣言しておきます。

```swift
protocol DragAndDropViewDelegate: class {
    func dragAndDropView(_ view: DragAndDropView,
                         didDragImageFileURLs draggedImages: [DraggedImage])
}
```

- 今回、ローカルに保存されている画像ファイルだけでなく画像自体をドラッグした場合にも対応したいため、ドラッグされるアイテムを下記のstructとして扱うようにします。

```swift
struct DraggedImage {
    let image: NSImage?
    let url: URL?
    var uti: String?  // 拡張子情報
}
```

- `NSView`のカスタムクラス`DragAndDropView`を作成し、ファイルがドラッグされたときの処理を書いていきます。
- 後ほど使用するプロパティを宣言します。

```swift
var delegate: DragAndDropViewDelegate?

// Viewがドラッグを許可するファイルのタイプ
let acceptableTypes: [NSPasteboard.PasteboardType] = [.fileURL, .tiff, .png, .URL, .string]
    
// URL先の画像のUTIが、NSImage.imageTypesのUTIに合致するかという条件
let filteringOptions: [NSPasteboard.ReadingOptionKey : Any] = [.urlReadingContentsConformToTypes : NSImage.imageTypes]

// ViewのUI制御用
var isDragging = false {
    didSet {
        needsDisplay = true
    }
}
```

- `View`上に画像がドラッグされている間、Viewの周りをハイライトさせるための処理。

```swift
override func draw(_ dirtyRect: NSRect) {
    super.draw(dirtyRect)
    
    // ドラッグされている場合にハイライトさせる
    if isDragging {
        NSColor.selectedControlColor.set()
    } else {
        NSColor.windowFrameColor.set()
    }
    
    let path = NSBezierPath(rect: bounds)
    path.lineWidth = 5
    path.stroke()
    
}
```

- ViewにDragできるファイルのタイプを設定します。

```swift
required init?(coder: NSCoder) {
    super.init(coder: coder)
    
    self.registerForDraggedTypes(acceptableTypes)
}
```

- ファイルがドラッグされた場合に処理を続けるかを判断するためのメソッドと、UTI判別用のファイルを宣言しておきます。

```swift
// MARK: - Helper Methods

func shouldAllowDrag(_ draggingInfo: NSDraggingInfo) -> Bool {
    var canAccept = false
    let pasteBoard = draggingInfo.draggingPasteboard
    
    if pasteBoard.canReadObject(forClasses: [NSURL.self], options: nil) ||
        pasteBoard.canReadObject(forClasses: [NSImage.self], options: nil) {
        canAccept = true
    }
    
    return canAccept
}

func uti(url: URL) -> String? {
    guard let r = try? url.resourceValues(forKeys: [.typeIdentifierKey]) else {
        return nil
    }
    return r.typeIdentifier
}
```

- 最後にドラッグに関する処理を実装します。

```swift
class DragAndDropView: NSView {
     // （省略）
     // MARK:- NSDraggingDestination Protocol Methods
    
    /// Viewの境界にファイルがドラッグされるときに呼ばれる
    /// 宛先がどのドラッグ操作を実行するのかを示す値を返す必要があります。
    override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
        let allow = shouldAllowDrag(sender)
        isDragging = allow
        
        // NSDragOperation.copy := The data represented by the image can be copied.
        return allow ? NSDragOperation.copy : NSDragOperation()
    }
    
    /// View上にファイルがドラッグで保持されている間、短い間隔毎に呼ばれるメソッド
    /// 宛先がどのドラッグ操作を実行するのかを示す値を返す必要があります。
    //    override func draggingUpdated(_ sender: NSDraggingInfo) -> NSDragOperation {
    //    }
    
    /// View上にファイルがドラッグされなくなった際に呼ばれる
    override func draggingExited(_ sender: NSDraggingInfo?) {
        isDragging = false
    }
    
    /// View上でファイルがドロップされた際に呼ばれる
    /// メッセージが返された場合はYES、performDragOperation:メッセージが送信されます。
    //    override func prepareForDragOperation(_ sender: NSDraggingInfo) -> Bool {
    //    }
    
    /// View上でファイルがドロップされた後の処理
    override func performDragOperation(_ draggingInfo: NSDraggingInfo) -> Bool {
        
        isDragging = false
        let pasteBoard = draggingInfo.draggingPasteboard
        
        // fileURLがドラッグされた場合
        if let urls = pasteBoard.readObjects(forClasses: [NSURL.self], options: filteringOptions) as? [URL],
            urls.count > 0,
            urls[0].isFileURL {
            var draggedImages = [DraggedImage]()
            for url in urls {
                let uti = self.uti(url: url)
                draggedImages.append(DraggedImage(image: nil, url: url, uti: uti))
            }
            
            delegate?.dragAndDropView(self, didDragImageFileURLs: draggedImages)
            return true
        }
        
        // 画像イメージがドラッグされた場合
        if let pasteboardItems = pasteBoard.pasteboardItems {
            var draggedImages = [DraggedImage]()
            for pasteboardItem in pasteboardItems {
                for type in pasteboardItem.types {
                    if let data = pasteboardItem.data(forType: type) {
                        if let image = NSImage(data: data) {
                            draggedImages.append(DraggedImage(image: image, url: nil, uti: type.rawValue))
                        }
                    }
                }
            }
            
            if draggedImages.count > 0 {
                delegate?.dragAndDropView(self, didDragImageFileURLs: draggedImages)
                return true
            }
        }
        
        // 画像のURL(http://~)がドラッグされた場合
        if let urls = pasteBoard.readObjects(forClasses: [NSURL.self], options: nil) as? [URL],
            urls.count > 0 {
            var draggedImages = [DraggedImage]()
            for url in urls {
                draggedImages.append(DraggedImage(image: nil, url: url, uti: nil))
            }
            
            delegate?.dragAndDropView(self, didDragImageFileURLs: draggedImages)
            return true
        }
        
        return false
    }
    
    /// 一連のドラッグ操作が完了したときに呼ばれる
    //    override func concludeDragOperation(_ sender: NSDraggingInfo?) {
    //    }
    
}
```