SwiftUI Appを使って、メニューバー付きのmacOSアプリケーションを設計し、ドラッグ&ドロップに対応させ、ビューを動的に更新（ApplicationDelegateAdaptor）

本ガイドでは、SwiftUI Appを使わずに、メニューバー付きのmacOSアプリケーションを開発する方法を解説します。
その後、ドラッグ&ドロップに対応させる方法を説明します。最後に、ドロップされたコンテンツに基づいてSwiftUIのビューを動的に更新する方法を紹介します。

![ezgif-5-6c99ec1c7f.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/85c7ba3a-6196-0d3a-d91b-64abacf825b1.gif)

以下のことが学べます：
1. SwiftUIアプリ内でのApplicationDelegateAdaptor機能の使用
2. MacOSメニューバー・アプリ向けのドラッグ&ドロップのサポートを追加する
3. アプリ内でのビューとクラスの間のコミュニケーション

## SwiftUIプロジェクトを新規に作成

Xcodeの最近のバージョンでは、SwiftUIアプリを作成することを選択した場合、
プロジェクトはAppDelegate.swiftファイルを持っていないのです。

アプリケーションを起動したときだけメニューバーボタンを作成したいので、
`applicationDidFinishLaunching`関数を使用する必要があります。
古い `AppDelegate.swift` ファイル内の関数にアクセスするアダプターを作成することで、
SwiftUIで`applicationDidFinishLaunching`関数を使用できます。

## App Delegateアダプタを追加する

SwiftUIアプリのコードに、以下のようなアダプタ変数を追加します。

```swift
@NSApplicationDelegateAdaptor(MenuBar_Drag_and_Drop_DemoApp_AppDelegate.self) var appDelegate
```

SwiftUIのアプリファイルは、以下のようになります。

```swift
@main
struct MenuBar_Drag_and_Drop_DemoApp: App {
    
    @NSApplicationDelegateAdaptor(MenuBar_Drag_and_Drop_DemoApp_AppDelegate.self) var appDelegate
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
    
}
```

MacOSのアプリなので、`NSApplicationDelegateAdaptor`を使用しました。iOSアプリの場合は、`UIApplicationDelegateAdaptor`を使う必要があります。

### `MenuBar_Drag_and_Drop_DemoApp_AppDelegate`を作成：

```swift
class MenuBar_Drag_and_Drop_DemoApp_AppDelegate: NSObject, NSApplicationDelegate {
    func applicationDidFinishLaunching(_ notification: Notification) {

    }
}
```

## アプリ起動時にメニューバーボタンを追加する

前回のQiitaの記事で説明したように、メニューバーアイコンを追加するコードを `MenuBar_Drag_and_Drop_DemoApp_AppDelegate` に追加することができます。

https://qiita.com/MaShunzhe/items/4c1c7ec79b83eee5df9b

```swift
class MenuBar_Drag_and_Drop_DemoApp_AppDelegate: NSObject, NSApplicationDelegate {
    
    var popover: NSPopover!
    var statusBarItem: NSStatusItem!
    
    func applicationDidFinishLaunching(_ notification: Notification) {
        // ポップオーバーの中にSwiftUIビューを設定
        let contentView = ContentView()
        // ポップオーバーを設定
        let popover = NSPopover()
        popover.behavior = .transient
        popover.contentViewController = NSHostingController(rootView: contentView)
        self.popover = popover
        // ステータスバーアイコンを設定
        self.statusBarItem = NSStatusBar.system.statusItem(withLength: CGFloat(NSStatusItem.variableLength))
        guard let button = self.statusBarItem.button else { return }
        button.image = NSImage(systemSymbolName: "checkmark.circle.fill", accessibilityDescription: nil)
        button.action = #selector(showHidePopover(_:))
    }
    
    @objc func showHidePopover(_ sender: AnyObject?) {
        guard let button = self.statusBarItem.button else { return }
        if self.popover.isShown {
            self.popover.performClose(sender)
        } else {
            self.popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
            self.popover.contentViewController?.view.window?.becomeKey()
        }
    }
    
}
```

## ドラッグ＆ドロップに対応

まず、ドロップ先はボタン（システムメニューバーに表示されるもの）なので、ドロップできるデータの種類を設定し、`NSApplicationDelegate`を登録する必要があります。

```swift
class MenuBar_Drag_and_Drop_DemoApp_AppDelegate: NSObject, NSApplicationDelegate {
    
   // ... code here ... //
    
    func applicationDidFinishLaunching(_ notification: Notification) {
        // ... code here ... //
        // Add drag and drop support
        button.window?.registerForDraggedTypes([.URL, .string])
        button.window?.delegate = self
        }

```

その後、`MenuBar_Drag_and_Drop_DemoApp_AppDelegate`クラスを3つのタイプ`NSPopoverDelegate`、`NSWindowDelegate`及び`NSDraggingDestination`に一致させます。

また、ドラッグ＆ドロップしたデータを処理する関数を書き込む必要もあります。

```swift
extension MenuBar_Drag_and_Drop_DemoApp_AppDelegate: NSPopoverDelegate, NSWindowDelegate, NSDraggingDestination {
    
    func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
        print(sender)
        return .link
    }
    
    func draggingUpdated(_ sender: NSDraggingInfo) -> NSDragOperation {
        return sender.draggingSourceOperationMask
    }
    
    func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
        /// Note: Only accept one dropped item
        if let firstObject = sender.draggingPasteboard.readObjects(forClasses: [NSURL.self])?.first {
            if let droppedItem = firstObject as? NSURL {
                print(droppedItem.absoluteString ?? "")
                self.showHidePopover(nil)
            }
            return true
        }
        return false
    }
    
    
}
```

これでURLをメニューバーアプリケーションにドラッグ＆ドロップできるようになります。

## SwiftUIビューとのコミュニケーション

これでドロップされたアイテムのURLを確認できます。
SwiftUIビューに対して当該URLを通知し、SwiftUIビューをダイナミックに更新することができます。

`ObservableObject`オブジェクトを使用してデータを保存できます。
`ObservableObject`内の変数を`@Published`修飾子でマーキングします。
これらの変数が変更されるたびに、SwiftUIビューにアップデートの通知が届きます。

```swift
class MenuBar_DroppedDataHelper: NSObject, ObservableObject {
    static let shared = MenuBar_DroppedDataHelper()
    @Published var droppedURLStr: String?
}
```

このアプリのすべてのビューまたはクラスが同じデータにアクセスできるように、このクラスを共有として作成しています。

この`ObservableObject`を`MenuBar_Drag_and_Drop_DemoApp_AppDelegate`内の変数として追加し、アダプターがデータにアクセスできるようにします。

```swift
class MenuBar_Drag_and_Drop_DemoApp_AppDelegate: NSObject, NSApplicationDelegate {
    
    var popover: NSPopover!
    var statusBarItem: NSStatusItem!
    
    @ObservedObject var dataHelper = MenuBar_DroppedDataHelper.shared

    // ... code here ...
    
}
```

これで、ドロップしたオブジェクトのURLが利用可能になると、その観測されたオブジェクト内の値を更新することができます。

```swift
if let firstObject = sender.draggingPasteboard.readObjects(forClasses: [NSURL.self])?.first {
    if let droppedItem = firstObject as? NSURL {
        print(droppedItem.absoluteString ?? "")
        self.dataHelper.droppedURLStr = droppedItem.absoluteString
        self.showHidePopover(nil)
    }
    return true
}
```

そして、SwiftUIの `ContentView()` で、
 `@ObservedObject` 変数を追加することによって、このデータにもアクセスできるようになります。

```swift
struct ContentView: View {
    
    @ObservedObject var dataHelper = MenuBar_DroppedDataHelper.shared
    
    var body: some View {
        
        VStack(alignment: .center) {
            
            Image(systemName: "checkmark.bubble.fill")
                .padding(.bottom)
            
            Text("Here's the dropped URL:")
            
            Text(dataHelper.droppedURLStr ?? "")
            
        }
        .padding()
        
    }
    
}
```

これで、リンクをドロップすると、SwiftUIのビューにURLが更新されます。

---
:relaxed: [Twitter @MszPro](https://twitter.com/MszPro)

:sunny: [私の公開されているQiita記事のリストをカテゴリー別にご覧いただけます。](http://mszpro.com/ioskiji/)
<img width="150" src="https://cdn.mszmagic.com/static-web-content/AppClipImage_small.png">