SwiftUIのメニューバーアプリで設定画面を出す
概要
本記事は、前回の記事の続きになりメニューバーアプリで設定画面を出せる様に実装してみたいと思います。
macOSバージョン毎の実装方法
実装方法としてはmacOSのバージョンによって以下の3通りある様です。
- macOS 14 以降
-
Settings+SettingsLinkを使う方法
-
- macOS 13 以降
-
WindowGroup+openWindow(id:)を使う方法
-
- レガシーな実装 (macOS 13 未満)
-
NSApp.sendAction(_:,to:from:)+showPreferencesWindow:を使う方法
-
今回はmacOS 14 以降を対象している想定なので、Settings Scene + SettingsLink を使って実装していきます。
Settings + SettingsLink
Settings
アプリの設定を見たり変更したりするためのインターフェイスを提示するシーン
SettingsLink
アプリによって定義された設定シーンを開くビュー
前回のプロジェクトをそのまま使って実装していきます。MenuBarExampleApp を以下に修正します。
import SwiftUI
@main
struct MenuBarExampleApp: App {
var body: some Scene {
MenuBarExtra(
"MenuBar Example",
systemImage: "menubar.rectangle"
) {
Group {
SettingsLink { Text("Preferences…") }
Divider()
Button("Quit") { NSApp.terminate(nil) }
}
}
Settings {
VStack {
Text("Settings")
}.padding(.all, 18)
}
}
}
実行するとアプリをクリックで開いた中に「Preferences…」ボタンがあり、押すと設定画面が表示されます。

ただ、メニューのみのアプリの場合はこれで良いのですが、menuBarExtraStyle(.window) でクリックすると何かしらのコンテンツが表示され、右クリックで設定などのメニューが開く様なアプリの場合は違った実装になってきます。
ContextMenu内のメニューから設定画面を開く
前回の記事の ContextMenu の箇所の続きにもなりますが、NSMenuItem で「Preferences…」メニューを登録し、クリックされたら設定画面を開く様にしてみます。
MenuBarExtraAccess を使って NSMenu でメニューを作成している為、上記の様に SettingsLink を使うことはできません。。そこで設定画面を開く openSettings というアクションが用意されているので、それを使って実装していこうと思います。
使う際は @Environment を使用してインスタンスを取得し、必要な場所で呼び出します。
例)
struct SettingsButton: View {
@Environment(\.openSettings) private var openSettings
var body: some View {
Button("Open Settings") {
openSettings()
}
}
}
これを NSMenuItem の Selector で渡さないといけないので、ちょっと工夫が必要です。今回は以下の様に実装しました。
まず MenuBarExampleApp に@Environment を使用して openSettings のインスタンスを取得します。
struct MenuBarExampleApp: App {
@Environment(\.openSettings) private var openSettings
// ...
}
次に Selector に渡すため以下のクラスを作成します。
class AppDelegate: NSObject {
static let shared = AppDelegate()
var openSettings: OpenSettingsAction?
@objc func openPreferences(_ sender: Any?) {
openSettings?()
}
}
var openSettings: OpenSettingsAction? となっているのは Viewツリー外でopenSettings のインスタンスを扱うと以下の様な警告が発生してしまう為、Viewツリー内で取得したインスタンスを設定するようにしています。
警告
Accessing Environment<OpenSettingsAction>'s value outside of being installed on a View. This will always read the default value and will not update.
最終的には以下の様な実装になりました。
import SwiftUI
import MenuBarExtraAccess
@main
struct MenuBarExampleApp: App {
@Environment(\.openSettings) private var openSettings
@State private var isPresented = false
@State private var statusItem: NSStatusItem?
var body: some Scene {
MenuBarExtra {
Text("Content")
} label: {
Image(systemName: "menubar.rectangle")
.onAppear {
AppDelegate.shared.openSettings = openSettings
}
}
.menuBarExtraStyle(.window)
.menuBarExtraAccess(isPresented: $isPresented) { item in
statusItem = item
addRightClickMonitor()
}
Settings {
VStack {
Text("Settings")
}.padding(.all, 18)
}
}
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()
let settingsItem = NSMenuItem(
title: "Preferences…",
action: #selector(AppDelegate.openPreferences),
keyEquivalent: ","
)
settingsItem.target = AppDelegate.shared
menu.addItem(settingsItem)
menu.addItem(.separator())
menu.addItem(withTitle: "Quit", action: #selector(NSApp.terminate(_:)), keyEquivalent: "q")
item.menu = menu
item.button?.performClick(nil)
item.menu = nil
}
}
class AppDelegate: NSObject {
static let shared = AppDelegate()
var openSettings: OpenSettingsAction?
@objc func openPreferences(_ sender: Any?) {
openSettings?()
}
}
実行するとクリックでコンテンツが表示され、右クリックのメニューから設定画面が表示されるかと思います。

参考URL
Discussion