SwiftUIでメニューバーアプリケーションを作成する(Xcode 13.1)


## サンプルはたくさんあるけれど・・・

Macのメニューバーに常駐するアプリケーションをSwiftUIで作るサンプルをいくつか探して試してみたのですが、現在のXcodeで作成できるSwiftアプリケーションのテンプレートだとうまくいかなかったため、さらにいろいろと駆けずり回ってどうにかサンプルが作れたので手順をメモしておきます。

### プロジェクト作成

#### macOSアプリとして作成します。

![スクリーンショット 2021-11-27 10.05.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58872/4cebe341-c9f1-d60b-97de-9940a0548f40.png)

#### Interfaceを `Storyboard` で作成します。 

![スクリーンショット 2021-11-27 10.06.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58872/89dd4fee-a738-b49a-a866-b04f300aac45.png)

#### 作成されたプロジェクト

![スクリーンショット 2021-11-27 10.07.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58872/a4ab0ca7-fd47-d4a6-5134-62f478f27ab7.png)

### ストーリーボードの修正

Mainを開いて、`Window Controller Scene` を選択して、`delete`キーで削除します。

![スクリーンショット 2021-11-27 10.08.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58872/5392c004-e1ab-d38c-926e-8f53ca5d1051.png)

![スクリーンショット 2021-11-27 10.10.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58872/d3152bcf-55dd-dd70-6710-833cef5a7895.png)

### アイコンの追加

Assetsを開いて、メニューバーに表示するアイコンを追加します。

![スクリーンショット 2021-11-27 10.18.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58872/f432f59b-7a2a-493f-1bb1-6c4a806af610.png)

追加されたアイコン

![スクリーンショット 2021-11-27 10.19.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58872/1badbb78-0fde-637f-0506-fd41bc4d4d29.png)

### info.plistの修正

`Application is agent (UIElement)` を `YES`に設定します。

![スクリーンショット 2021-11-27 10.12.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58872/a01bef0d-bc14-ba30-4cfa-07fce0e8569c.png)

### ソースの修正

#### StatusBarControllerの追加

メニューバーの表示＆制御を行うコントローラを追加します。

```swift:StatusBarController.swift
import Foundation
import AppKit

class StatusBarController {
    private var statusBar: NSStatusBar
    private var statusItem: NSStatusItem
    private var popover: NSPopover
    
    init(_ popover: NSPopover) {
        self.popover = popover
        statusBar = NSStatusBar.init()
        // Creating a status bar item having a fixed length
        statusItem = statusBar.statusItem(withLength: 28.0)
        
        if let statusBarButton = statusItem.button {
            // 追加したアイコン名を設定
            statusBarButton.image = #imageLiteral(resourceName: "icon")
            statusBarButton.image?.size = NSSize(width: 18.0, height: 18.0)
            statusBarButton.image?.isTemplate = true            
            statusBarButton.action = #selector(togglePopover(sender:))
            statusBarButton.target = self
        }
    }
    
    @objc func togglePopover(sender: AnyObject) {
        if(popover.isShown) {
            hidePopover(sender)
        }
        else {
            showPopover(sender)
        }
    }
    
    func showPopover(_ sender: AnyObject) {
        if let statusBarButton = statusItem.button {
            popover.show(relativeTo: statusBarButton.bounds, of: statusBarButton, preferredEdge: NSRectEdge.maxY)
        }
    }
    
    func hidePopover(_ sender: AnyObject) {
        popover.performClose(sender)
    }
}
```

### AppDelegateの修正

```swift:AppDelegate.swift
import Cocoa
import SwiftUI

@main
class AppDelegate: NSObject, NSApplicationDelegate {

    var popover = NSPopover.init()
    var statusBar: StatusBarController?

    func applicationDidFinishLaunching(_ aNotification: Notification) {
        // Create the SwiftUI view that provides the contents
        let contentView = ContentView()

        // Set the SwiftUI's ContentView to the Popover's ContentViewController
        popover.contentSize = NSSize(width: 360, height: 360)
        popover.contentViewController = NSHostingController(rootView: contentView)
        
        // Create the Status Bar Item with the above Popover
        statusBar = StatusBarController.init(popover)
    }

    func applicationWillTerminate(_ aNotification: Notification) {
        // Insert code here to tear down your application
    }
}
```

#### Viewを作成する

メニューバーに表示されたアイコンをクリックしたときに表示する画面を作成します。

```swift:ContentView.swift

import SwiftUI

struct ContentView: View {
    var body: some View {
        Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
            .background(Color.yellow)
            .foregroundColor(Color.blue)
            .frame(width: 300, height: 150)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
```

### 実行結果

実行した結果です(暗くてわかりずらいですが)

![menubar.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58872/d146a690-f7eb-3f9d-d098-3d95749103a7.gif)

### まとめ

SwiftUIによる画面の開発が容易になったため、iOSだけでなくMacOSでなにかアプリを作ってみようかなと思いメニューバーアプリケーションとしてユーティリティーの作り方をいろいろと調べてみたのですが、年代やらXcodeのバージョンによっていろいろと作りが変わるようで、だいぶ沼化しました。

この作り方ももしかしたら他のやり方があるのかもしれませんが、どなたかのモチベーションを刺激できたら幸いですｗ

今回のサンプルはGitHubでも公開しています。
https://github.com/6in/MenuBarAppSample