SwiftUIでメニューバーアプリを作成する
概要
今回はSwiftUI の MenuBarExtra
を使って “メニューバー常駐アプリ” を試してみました。簡単なサンプルと最後に右クリックでのContextMenu表示も試してみてます。
動作環境
MBA M3 24GB
Sequoia 15.5
Xcode: 16.1 (16B40)
メニューバーに関しては説明するまでもないと思いますが、メニューバーの中には「アプリメニュー」と「メニューバーエクストラ」が表示されます。
-
アプリメニュー
- 何らかのアプリを開いている時に出るメニュー
- 例) Chrome
-
メニューバーエクストラ
- 右側に表示されるアプリ固有の機能を提示するメニュー
- 右側に表示されるアプリ固有の機能を提示するメニュー
今回は「メニューバーエクストラ」をSwiftUIで作成していこうと思います。
メニューバーエクストラの仕様に関して重要そうな箇所を以下にピックアップしました。
- アプリのメニュー用のスペースが足りないときは、必要に応じてメニューバーエクストラが非表示になります
- メニューバーエクストラが多すぎる場合、アプリのメニュー表示が窮屈にならないように一部のメニューバーエクストラが非表示になることがあります
MenuBarExtra
macOS 13.0+
から使えて、その名の通りメニューバーエクストラにアプリを表示してくれる MenuBarExtra
が用意されています。今回はこちらを使ってメニューバーアプリを作成していきたいと思います。
プロジェクト作成
Xcodeを起動し、「Create New Project…」>「macOS」>「App」を選択します。
今回はProductName「MenuBarExample」として作成しました。
最低限の実装
まずはただメニューバーにアプリが表示され、クリックするとテキストが表示されるだけのアプリを実装してみたいと思います。
- 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()
}
これを実行すると以下の様なシンプルなアプリが起動されます。
menuBarExtraStyle
MenuBarExtra
の menuBarExtraStyle
で指定できる値は以下の2つになります。
- menuBarExtraStyle
- window:
- 先ほど実装したもの
- コンテンツに応じて動的にサイズを変更することも、ルートビューに固定フレームを設定することもできます
- menu:
- automatic: ↑のmenuと同じになりました
- window:
クリップボードコピー
次は入力テキストをクリップボードに大文字にしてコピーするサンプルを実装してみたいと思います。
- 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が表示されます。
実際にテキスト入力して、「Copy uppercased result」ボタンを押すと大文字でコピーされます。
scrollContentBackground の説明は👇
LSUIElement
このままだと Dockとアプリケーションスイッチャー(Cmd+Tab) に表示されてしまうので、メニューバーだけのアプリの場合、非表示が望ましいかと思います。
Info.plist に Application is agent
項目を追加し値を true
にしてあげれば非表示になるので設定しときます。
XCode上で「Target」>「Info」>「Application is agent (UIElement)」を追加し値を「YES」に設定します。
実行するとDockやアプリケーションスイッチャーに表示されなくなるかと思います。
ContextMenu
このままだと終了させる事ができないアプリになってしまうので、右クリックでContextMenuを表示させ、メニューからアプリを終了できるようにしてみたいと思います。
今回は MenuBarExtraAccess というパッケージを使って実装してみたいと思います。
まずはパッケージを追加します。XCode上で「File」>「Add Package Dependencies…」を選択します。右上の検索窓に MenuBarExtraAccess
のGithubのURLをコピペします。
するとパッケージが表示されるので「Add Package」で追加します。
その際に「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が表示されているかと思います。
実際に「Quit」をクリックするとアプリが終了します。(Preferences…は仮で置いてるだけです)
またContextMenuが開いた状態で「Cmd+Q」のショートカットでもアプリが終了します。
参考URL
Discussion