【Swift】SwiftUIでメニューバーアプリの作り方

# はじめに
私はGitHubの草をメニューバーから確認できるというメニューバーアプリを作成しました。
<img width="326" alt="app-screenshot-1" src="https://user-images.githubusercontent.com/84154073/170965822-14526622-026b-4466-8297-93c4cea6301f.png">

アプリURL

https://apps.apple.com/jp/app/Contributions-Graphs-in-Menu/id1622632080

GitHubリポジトリ

https://github.com/SNQ-2001/github-contributions-menubar-macos

今回は一通りサンプルアプリを作成しながら解説していこうと思います。
躓きポイントやリジェクトポイントがいくつかあるので参考になれば嬉しいです

# プロジェクト作成
①「macOS」を選択します(Multiplatformでも可)
②「App」を選択します
<img alt="スクリーンショット 2022-05-30 18 50 35" src="https://user-images.githubusercontent.com/84154073/170967044-4da9ab43-23d6-4ba5-b23d-4f62bd1a49f3.png">

「Product Name」を入力します
今回は「SwiftUI-MenuBar-Demo」というプロジェクト名にしました。
<img width="1369" alt="スクリーンショット 2022-05-30 18 54 02" src="https://user-images.githubusercontent.com/84154073/170967597-d4ac588e-95db-42dc-a496-6addc7fe3928.png">

# 実装
## Appの編集
まず、`ContentView`が表示されないように変更します。
`WindowGroup`から`Settings`に変更することでViewが表示されなくなります。
```diff_swift
import SwiftUI

@main
struct SwiftUI_MenuBar_DemoApp: App {
    var body: some Scene {
-        WindowGroup {
-            ContentView()
-        }
+        Settings { }
    }
}
```
SwiftUIには`AppDelegate`がないので追加します
```diff_swift
import SwiftUI

@main
struct SwiftUI_MenuBar_DemoApp: App {
+    @NSApplicationDelegateAdaptor(AppDelegate.self) var delegate
    var body: some Scene {
        Settings { }
    }
}

+class AppDelegate: NSObject, NSApplicationDelegate {
+    var statusBarItem: NSStatusItem!
+    var popover = NSPopover()
+    func applicationDidFinishLaunching(_ notification: Notification) {
+        popover.behavior = .transient
+        popover.contentViewController = NSHostingController(rootView: ContentView())
+        self.statusBarItem = NSStatusBar.system.statusItem(withLength: CGFloat(NSStatusItem.variableLength))
+        guard let button = self.statusBarItem.button else { return }
+        // アイコンの設定
+        button.image = NSImage(systemSymbolName: "camera.macro", accessibilityDescription: nil)
+        // アクションの設定
+        button.action = #selector(menuButtonAction(sender:))
+    }
+    @objc func menuButtonAction(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?.makeKey()
+        }
+    }
+}
```
:::note info
ポイント
表示されたView以外をタップすると自動で消えるようになります
```swift
self.popover.contentViewController?.view.window?.makeKey()
```
:::

:::note alert
注意
こちらのコードでも同じような事が可能ですが、
2回目以降の表示でおかしな挙動が出るので使わない方が良いです
```swift
NSApp.activate(ignoringOtherApps: true)
```
:::
## ContentViewの編集
続いて`ContentView`に`frame`を指定します
```ContentView.swift
import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Hello, world!")
        }
        .frame(width: 200, height: 110)
    }
}
```
それっぽくなってきましたね
![Videotogif (1)](https://user-images.githubusercontent.com/84154073/170975187-e695d9b9-4342-4182-8b7e-d7ea3483a94a.gif)
## Info.plistの編集
メニューバーに常駐させておく為の設定を行います。
<img alt="スクリーンショット 2022-05-30 19 41 51" src="https://user-images.githubusercontent.com/84154073/170976049-078f0e57-b363-4b3f-8b1a-389c88c3dd0a.png">

<img alt="スクリーンショット 2022-05-30 19 44 25" src="https://user-images.githubusercontent.com/84154073/170976299-9096da2b-5b60-4889-aca1-3076eaabe716.png">

以下の値を設定します。
|Key|Type|Value|
|-|-|-|
|Application is agent (UIElement)|Boolean|Yes|

ソースコードだとこのようになってます
```Info.plist
<dict>
	<key>LSUIElement</key>
	<true/>
</dict>
```

# 追加機能
- 左クリックではViewを表示
- 右クリックでメニューを表示

上記の仕様にしてみたいと思います。

```diff_swift
import SwiftUI

@main
struct SwiftUI_MenuBar_DemoApp: App {
    @NSApplicationDelegateAdaptor(AppDelegate.self) var delegate
    var body: some Scene {
        Settings { }
    }
}

class AppDelegate: NSObject, NSApplicationDelegate {
    var statusBarItem: NSStatusItem!
    var popover = NSPopover()
    func applicationDidFinishLaunching(_ notification: Notification) {
        popover.behavior = .transient
        popover.contentViewController = NSHostingController(rootView: ContentView())
        self.statusBarItem = NSStatusBar.system.statusItem(withLength: CGFloat(NSStatusItem.variableLength))
        guard let button = self.statusBarItem.button else { return }
        // アイコンの設定
        button.image = NSImage(systemSymbolName: "camera.macro", accessibilityDescription: nil)
        // アクションの設定
        button.action = #selector(menuButtonAction(sender:))
+        // 右クリックの設定
+        button.sendAction(on: [.leftMouseUp, .rightMouseUp])
    }
    @objc func menuButtonAction(sender: AnyObject) {
+        guard let event = NSApp.currentEvent else { return }
+        if event.type == NSEvent.EventType.rightMouseUp {
+            let menu = NSMenu()
+            menu.addItem(
+                withTitle: NSLocalizedString("ボタン1", comment: ""),
+                action: #selector(button1),
+                keyEquivalent: ""
+            )
+            menu.addItem(
+                withTitle: NSLocalizedString("ボタン2", comment: ""),
+                action: #selector(button2),
+                keyEquivalent: ""
+            )
+            statusBarItem.menu = menu
+            statusBarItem.button?.performClick(nil)
+            statusBarItem.menu = nil
+            return
+        } else if event.type == NSEvent.EventType.leftMouseUp {
            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?.makeKey()
            }
+        }
    }

+    @objc func button1() {
+        print("ボタン1です")
+    }

+    @objc func button2() {
+        print("ボタン2です")
+    }

}
```

こんな感じになりました
![Videotogif (2)](https://user-images.githubusercontent.com/84154073/170982007-0f57bfda-71e4-45c0-a5d1-d444324e969e.gif)


# リリースに向けて
リリースするとなると「アプリを終了ボタン」の実装が必須になります。
私はここで何度もリジェクトされました。
なぜなら、アプリ終了のやり方がわからなかったから！！

こうやるみたいです
```swift
Button("Quit") {
    NSApp.terminate(self)
}
```

ここで注意ポイントがあります
Viewがある場合はView側に「アプリ終了」ボタンを置かないとダメなようです。

Viewがあるのにメニューにアプリ終了ボタンを置くとユーザーを誤解させるUIという理由でリジェクトされます。

# おわり
今回作成したプロジェクトです

https://github.com/SNQ-2001/SwiftUI-MenuBar-Demo

こちらはリリース済みのGitHubの草を確認できるメニューバーアプリです
こちらも参考にしてみてください

https://github.com/SNQ-2001/github-contributions-menubar-macos