メニューバーに常駐するmacOSアプリを作成し、画像をドラッグ・アンド・ドロップで認識できるようにする

# 概要

- Macの右上のメニューバーにアイコンを表示するアプリを作成します。
- また最終的に、そのアイコン上に画像ファイルをドラッグ・アンド・ドロップできるよう実装を行います。
- 上のバーの名称は以下メニューバーと呼ぶこととします。
    -  [Macのデスクトップ、メニューバー、ヘルプ](https://support.apple.com/ja-jp/guide/macbook-air/apd65991c417/mac)

<img width="512" alt="image" src="https://i.imgur.com/sMMhvkd.jpg">

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

![dragdemo](https://i.imgur.com/5Hd8pPd.gif)

# 基本的な実装
- 以下の記事の通り実装を行います。
    - [Menus and Popovers in Menu Bar Apps for macOS](https://www.raywenderlich.com/450-menus-and-popovers-in-menu-bar-apps-for-macos)

```swift
class AppDelegate: NSObject, NSApplicationDelegate {
    
    let statusItem = NSStatusBar.system.statusItem(withLength:NSStatusItem.squareLength)
    
    func applicationDidFinishLaunching(_ aNotification: Notification) {
        
        if let statusBarButton = statusItem.button {
            statusBarButton = NSImage(named:NSImage.Name("StatusBarButtonImage"))
        }
        
        constructMenu()
    }
    
    @objc func printQuote(_ sender: Any?) {
        let quoteText = "Never put off until tomorrow what you can do the day after tomorrow."
        let quoteAuthor = "Mark Twain"
        
        print("\(quoteText) — \(quoteAuthor)")
    }
    
    func constructMenu() {
        let menu = NSMenu()
        
        menu.addItem(NSMenuItem(title: "Print Quote", action: #selector(AppDelegate.printQuote(_:)), keyEquivalent: "P"))  // 大文字なので、Commant + "Shitf + p"となる
        menu.addItem(NSMenuItem.separator())
        menu.addItem(NSMenuItem(title: "Quit Quotes", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q"))
        
        statusItem.menu = menu
    }
}
```

- これより以下は、記事の内容外の実装を紹介します。

# メニュー項目にサブメニューを持たせる
- 以下のようなメニューを入れ子にすることを考えます。

![-w270](https://i.imgur.com/dTzZfka.jpg)

- クラスとしては、下記のように入れ子になっています

![-w667](https://i.imgur.com/G8lF8Eh.jpg)

- `submenu`として追加するのがポイントです。
    - [How to add NSMenu Programmatically?](https://stackoverflow.com/questions/28781974/how-to-add-nsmenu-programmatically)

```swift
func constructMenu() {
    let menu = NSMenu()
    menu.addItem(NSMenuItem(title: "Parent_01", action: #selector(AppDelegate.printQuote(_:)), keyEquivalent: ""))
    menu.addItem(NSMenuItem(title: "Parent_02", action: #selector(NSApplication.terminate(_:)), keyEquivalent: ""))
    menu.addItem(NSMenuItem.separator())
    
    // サブメニューを持つNSMenuItemを作成する
    let paren_03 = NSMenuItem(title: "Parent_03", action: #selector(AppDelegate.printQuote(_:)), keyEquivalent: "")
    let menuForParent = NSMenu()
    menuForParent.addItem(NSMenuItem(title: "Children_01", action: #selector(AppDelegate.printQuote(_:)), keyEquivalent: ""))
    menuForParent.addItem(NSMenuItem.separator())
    menuForParent.addItem(NSMenuItem(title: "Children_02", action: #selector(NSApplication.terminate(_:)), keyEquivalent: ""))
    paren_03.submenu = menuForParent
    
    menu.addItem(paren_03)
    
    statusItem.menu = menu
}
```

# メニューアイコンにファイルをドラッグ・アンド・ドロップする
## 概要
- 以下のようにメニューアイコンに画像ファイルをドラッグし、アプリ側で認識するよう実装を行います。

![dragdemo](https://i.imgur.com/5Hd8pPd.gif)

- 以下の方針を参考に実装します。
    - [Swift NStatusBarItem, make button support drag&drop](https://stackoverflow.com/questions/61687206/swift-nstatusbaritem-make-button-support-dragdrop)
- [NSDraggingDestination](https://developer.apple.com/documentation/appkit/nsdraggingdestination)に適合しているのは下記のクラスです。

>Conforming Types
>NSCollectionView, NSView

- また[NSButton](https://developer.apple.com/documentation/appkit/nsbutton)、もっというと今回は[NSStatusBarButton](https://developer.apple.com/documentation/appkit/nsstatusbarbutton)は`NSView`を継承しています。
- よって結論としては、[NSStatusBarButton](https://developer.apple.com/documentation/appkit/nsstatusbarbutton)の[NSDraggingDestination](https://developer.apple.com/documentation/appkit/nsdraggingdestination)のメソッドをoverrideしてやれば良いですね。
- 当初はカスタムクラスを作ろうとしましたが、<br>[NSStatusItem.NSStatusBarButton](https://developer.apple.com/documentation/appkit/nsstatusitem/1535056-button)が`get only`なので、extensionで動作をoverrideを行いました。

## 実装
- AppDelegate外でアイコンを変更するため、publicメソッド`changeMenubarIconImage`を用意しておく。
    - Notificationを使うほうが疎でいいかも。

```swift
class AppDelegate: NSObject, NSApplicationDelegate {
    
    public let statusItem = NSStatusBar.system.draggableStatusItem(withLength: NSStatusItem.squareLength)
    
    public func changeMenubarIconImage(isDragging isDragIcon: Bool) {
        if let statusBarButton = statusItem.button {
            if isDragIcon {
                statusBarButton.image = NSImage(named:NSImage.Name("StatusBarButtonImageDragged"))
            } else {
                statusBarButton.image = NSImage(named:NSImage.Name("StatusBarButtonImage"))
            }
        }
    }
    
    public func receiveImageDraggedOnMenubarIcon(for urls: [URL]) {
        print("🍎URL Detected!")
        for url in urls {
            print(url)
        }
    }
    ...
```

- 特定のファイル形式を受け取るための`NSStatusItem`を返すメソッドを用意しておきます。
    - この辺は「[ファイルをNSViewにドラッグしてパスを取得する \(Swift\) \- Qiita](https://qiita.com/IKEH/items/1cdf51591be506c3f74b)」と同じですね。
- `registerForDraggedTypes`でドラッグできるアイテムのタイプを指定します。

```swift
extension NSStatusBar {
    
    func draggableStatusItem(withLength length: CGFloat) -> NSStatusItem {
        let statusItem = self.statusItem(withLength: length)
        
        if let button = statusItem.button {
            button.image = NSImage(named:NSImage.Name("StatusBarButtonImage"))
            button.registerForDraggedTypes([.fileURL, .tiff, .png, .URL, .string])
        }
        
        return statusItem
    }
}

```

- こちらも「[ファイルをNSViewにドラッグしてパスを取得する \(Swift\) \- Qiita](https://qiita.com/IKEH/items/1cdf51591be506c3f74b)」と同様です。
- 要するに、ファイルのドラッグ状態によって、メニューバーのアイコンを切り替えることをしています。

```swift
extension NSStatusBarButton {
    
    
    // MARK: - Helper Methods
    
    private func filteringOptions() -> [NSPasteboard.ReadingOptionKey : Any] {
        return [.urlReadingContentsConformToTypes : NSImage.imageTypes]
    }
    
    private func shouldAllowDrag(_ draggingInfo: NSDraggingInfo) -> Bool {
        var canAccept = false
        let pasteBoard = draggingInfo.draggingPasteboard
        
        if pasteBoard.canReadObject(forClasses: [NSURL.self], options: filteringOptions()) ||
            pasteBoard.canReadObject(forClasses: [NSImage.self], options: filteringOptions()) {
            canAccept = true
        }
        
        return canAccept
    }
    
    private func setDraggingInfo(isDragging: Bool) {
        if let appDelegate = NSApplication.shared.delegate as? AppDelegate {
            appDelegate.changeMenubarIconImage(isDragging: isDragging)
        }
    }
    
    
    // MARK:- NSDraggingDestination Methods
    
    open override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
        print("draggingEntered")
        
        let allow = shouldAllowDrag(sender)
        setDraggingInfo(isDragging: allow)
        
        return allow ? .copy : NSDragOperation()
    }
    
    
    open override func draggingExited(_ sender: NSDraggingInfo?) {
        setDraggingInfo(isDragging: false)
    }
    
    
    open override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
        
        let pasteBoard = sender.draggingPasteboard
        
        // 今回はfileURLがドラッグされた場合のみ実装
        if let urls = pasteBoard.readObjects(forClasses: [NSURL.self], options: filteringOptions()) as? [URL],
            urls.count > 0,
            urls[0].isFileURL {
            
            if let appDelegate = NSApplication.shared.delegate as? AppDelegate {
                appDelegate.receiveImageDraggedOnMenubarIcon(for: urls)
            }
            
            return true
        }
        
        return false
    }
    
    
    open override func draggingEnded(_ sender: NSDraggingInfo) {
        setDraggingInfo(isDragging: false)
        print("draggingEnded")
    }
}
```

## 余談
- [registerForDraggedTypes with NSStatusBarButton?](https://stackoverflow.com/questions/31370334/registerfordraggedtypes-with-nsstatusbarbutton)
    - `NSStatusBarButton`にドラッグ・アンド・ドロップ可能な`NSView`を`addSubview`すればと回答
    - 試しましたが、アイコンの外観は変更はできましたが、ドラッグのアクションを受け取るのが不可でした。
        - `Responder Chain`あたりを考えるとうまくいくのだろうか…？

# メニューバーのアイコンを非表示にする
- [isVisible](https://developer.apple.com/documentation/appkit/nsstatusitem/1644025-isvisible)を設定等から設定できるようにすれば良さそうです。

```swift
statusItem.isVisible = false  // で非表示になる
```

- [OS X のメニューバーに常駐するメニューエクストラを作る](https://qiita.com/usagimaru/items/bbf2f394dc532424330d)

>ステータスメニューを実装する際にはそれを非表示にすることができるオプションを環境設定などに用意しておくべきです。

# その他参考した記事

- [カスタムなウィンドウを表示した状態でNSStatusItemをハイライトする \- a\.out](https://questbeat.hatenablog.jp/entry/2013/07/17/232454)
- [Mac \- メニューバーのアイコンを並び替え・削除](https://pc-karuma.net/mac-menu-bar-icons-rearrange-delete/)
    - Commandを押しながらでアイコンの順番を入れ替えれます。
- [ターゲットのアイコン4](https://icooon-mono.com/12790-%e3%82%bf%e3%83%bc%e3%82%b2%e3%83%83%e3%83%88%e3%81%ae%e3%82%a2%e3%82%a4%e3%82%b3%e3%83%b34/)