🍎

SwiftUIでメニューバーアプリを作成する

に公開

概要

今回はSwiftUI の MenuBarExtra を使って “メニューバー常駐アプリ” を試してみました。簡単なサンプルと最後に右クリックでのContextMenu表示も試してみてます。

動作環境

MBA M3 24GB
Sequoia 15.5
Xcode: 16.1 (16B40)

https://developer.apple.com/jp/design/human-interface-guidelines/the-menu-bar

メニューバーに関しては説明するまでもないと思いますが、メニューバーの中には「アプリメニュー」と「メニューバーエクストラ」が表示されます。

  • アプリメニュー

    • 何らかのアプリを開いている時に出るメニュー
    • 例) Chrome
      image1.png
  • メニューバーエクストラ

    • 右側に表示されるアプリ固有の機能を提示するメニュー
      image2.png

今回は「メニューバーエクストラ」をSwiftUIで作成していこうと思います。

メニューバーエクストラの仕様に関して重要そうな箇所を以下にピックアップしました。

  • アプリのメニュー用のスペースが足りないときは、必要に応じてメニューバーエクストラが非表示になります
  • メニューバーエクストラが多すぎる場合、アプリのメニュー表示が窮屈にならないように一部のメニューバーエクストラが非表示になることがあります

MenuBarExtra

https://developer.apple.com/documentation/SwiftUI/MenuBarExtra

macOS 13.0+ から使えて、その名の通りメニューバーエクストラにアプリを表示してくれる MenuBarExtra が用意されています。今回はこちらを使ってメニューバーアプリを作成していきたいと思います。

プロジェクト作成

Xcodeを起動し、「Create New Project…」>「macOS」>「App」を選択します。

image3.png

今回はProductName「MenuBarExample」として作成しました。

image4.png

最低限の実装

まずはただメニューバーにアプリが表示され、クリックするとテキストが表示されるだけのアプリを実装してみたいと思います。

  • MenuBarExampleApp.swift
import SwiftUI

@main
struct MenuBarExampleApp: App {
    var body: some Scene {
        MenuBarExtra(
            "MenuBar Example",
            systemImage: "menubar.rectangle"
        ) {
            ContentView()
                .frame(width: 300, height: 180)
        }
        .menuBarExtraStyle(.window)
    }
}
  • ContentView.swift
import SwiftUI

struct ContentView: View {

    var body: some View {
        Text("Hi")
    }
}

#Preview {
    ContentView()
}

これを実行すると以下の様なシンプルなアプリが起動されます。

image5.png

MenuBarExtramenuBarExtraStyle で指定できる値は以下の2つになります。

  • menuBarExtraStyle
    • window:
      • 先ほど実装したもの
      • コンテンツに応じて動的にサイズを変更することも、ルートビューに固定フレームを設定することもできます
    • menu:
      image6.png
      • automatic: ↑のmenuと同じになりました

クリップボードコピー

次は入力テキストをクリップボードに大文字にしてコピーするサンプルを実装してみたいと思います。

  • ContentView.swift
import SwiftUI

struct ContentView: View {
    @State private var textInput: String = ""

    var body: some View {
        VStack(alignment: .leading) {
            Text("Add your text below:")
                .foregroundStyle(.secondary)
            TextEditor(text: $textInput)
                .padding(.vertical, 4)
                .scrollContentBackground(.hidden)
                .background(.thinMaterial)
            Button(
                "Copy uppercased result",
                systemImage: "square.on.square"
            ) {
                let pasteboard = NSPasteboard.general
                pasteboard.clearContents()
                pasteboard.setString(textInput.uppercased(), forType: .string)
            }
            .buttonStyle(.plain)
            .foregroundStyle(.blue)
            .bold()
        }
        .padding()
    }
}

#Preview {
    ContentView()
}

実行すると👇のようなViewが表示されます。

image7.png

実際にテキスト入力して、「Copy uppercased result」ボタンを押すと大文字でコピーされます。

image8.png

scrollContentBackground の説明は👇

https://zenn.dev/tsuzuki817/articles/fb5f6261945af6

LSUIElement

このままだと Dockとアプリケーションスイッチャー(Cmd+Tab) に表示されてしまうので、メニューバーだけのアプリの場合、非表示が望ましいかと思います。

Info.plist に Application is agent 項目を追加し値を true にしてあげれば非表示になるので設定しときます。

XCode上で「Target」>「Info」>「Application is agent (UIElement)」を追加し値を「YES」に設定します。

image9.png

実行するとDockやアプリケーションスイッチャーに表示されなくなるかと思います。

ContextMenu

このままだと終了させる事ができないアプリになってしまうので、右クリックでContextMenuを表示させ、メニューからアプリを終了できるようにしてみたいと思います。

今回は MenuBarExtraAccess というパッケージを使って実装してみたいと思います。
https://github.com/orchetect/MenuBarExtraAccess

まずはパッケージを追加します。XCode上で「File」>「Add Package Dependencies…」を選択します。右上の検索窓に MenuBarExtraAccess のGithubのURLをコピペします。

するとパッケージが表示されるので「Add Package」で追加します。

image10.png

その際に「Add to Target」でTargetを指定するのを忘れずに設定しときます。

準備ができたら MenuBarExampleApp.swift にContextMenuの実装を追加します。

import SwiftUI
import MenuBarExtraAccess

@main
struct MenuBarExampleApp: App {
    @State private var isPresented = false
    @State private var statusItem: NSStatusItem?
    var body: some Scene {
        MenuBarExtra(
            "MenuBar Example",
            systemImage: "menubar.rectangle"
        ) {
            ContentView()
                .frame(width: 300, height: 180)
        }
        .menuBarExtraStyle(.window)
        .menuBarExtraAccess(isPresented: $isPresented) { item in
            statusItem = item
            addRightClickMonitor()
        }
    }
    
    private func addRightClickMonitor() {
        guard let item = statusItem else { return }
        NSEvent.addLocalMonitorForEvents(matching: .rightMouseDown) { event in
            if event.window == item.button?.window {
                popupContextMenu(for: item)
                return nil
            }
            return event
        }
    }

    private func popupContextMenu(for item: NSStatusItem) {
        let menu = NSMenu()
        menu.addItem(withTitle: "Preferences…", action: nil, keyEquivalent: ",")
        menu.addItem(.separator())
        menu.addItem(withTitle: "Quit", action: #selector(NSApp.terminate(_:)), keyEquivalent: "q")
        item.menu = menu
        item.button?.performClick(nil)
        item.menu = nil
    }
}

実行して、右クリックするとContextMenuが表示されているかと思います。

image11.png

実際に「Quit」をクリックするとアプリが終了します。(Preferences…は仮で置いてるだけです)

またContextMenuが開いた状態で「Cmd+Q」のショートカットでもアプリが終了します。

参考URL

https://nilcoalescing.com/blog/BuildAMacOSMenuBarUtilityInSwiftUI/

https://stackoverflow.com/questions/76372729/add-contextmenu-to-menubarextra-swiftui-macos

Discussion