例1:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 |
import SwiftUI import AppKit // メニューバー管理クラス @Observable class MenuBarManager { var isMenuBarMode: Bool { didSet { UserDefaults.standard.set(isMenuBarMode, forKey: "isMenuBarMode") updateMenuBarStatus() } } private var statusItem: NSStatusItem? private var popover: NSPopover? private var isInitialized = false init() { // 保存された設定を読み込み(この時点では適用しない) self.isMenuBarMode = UserDefaults.standard.bool(forKey: "isMenuBarMode") } // アプリ起動後に呼び出す初期化メソッド func initialize() { guard !isInitialized else { return } isInitialized = true if isMenuBarMode { // 少し遅延させてウィンドウが作成されるのを待つ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in self?.updateMenuBarStatus() } } } private func setupMenuBar() { // ステータスバーアイテムを作成 statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) if let button = statusItem?.button { button.image = NSImage(systemSymbolName: "app.fill", accessibilityDescription: "アプリ") button.action = #selector(togglePopover) button.target = self } // ポップオーバーを作成 popover = NSPopover() popover?.contentSize = NSSize(width: 400, height: 500) popover?.behavior = .transient popover?.contentViewController = NSHostingController(rootView: MenuBarContentView(menuBarManager: self)) } private func removeMenuBar() { if let statusItem = statusItem { NSStatusBar.system.removeStatusItem(statusItem) self.statusItem = nil } popover = nil } @objc private func togglePopover() { guard let button = statusItem?.button else { return } if let popover = popover { if popover.isShown { popover.performClose(nil) } else { popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY) } } } private func updateMenuBarStatus() { if isMenuBarMode { setupMenuBar() // メインウィンドウを隠す NSApp.windows.first?.orderOut(nil) } else { removeMenuBar() // メインウィンドウを表示 if let window = NSApp.windows.first { window.makeKeyAndOrderFront(nil) NSApp.activate(ignoringOtherApps: true) } } } func showMainWindow() { isMenuBarMode = false } } // メインアプリ @main struct MenuBarApp: App { @State private var menuBarManager = MenuBarManager() var body: some Scene { WindowGroup { ContentView(menuBarManager: menuBarManager) } .commands { // メニューバーのコマンド追加 CommandGroup(after: .appInfo) { Button("メニューバーから開く") { menuBarManager.showMainWindow() } .keyboardShortcut("m", modifiers: [.command, .shift]) } } } } // メインビュー struct ContentView: View { @Bindable var menuBarManager: MenuBarManager var body: some View { VStack(spacing: 20) { Text("メニューバー常駐アプリ") .font(.title) Divider() Toggle("メニューバーに常駐", isOn: $menuBarManager.isMenuBarMode) .toggleStyle(.switch) if menuBarManager.isMenuBarMode { VStack(alignment: .leading, spacing: 8) { Label("メニューバーのアイコンをクリックしてアプリを開けます", systemImage: "info.circle") .font(.caption) .foregroundColor(.secondary) Label("⌘⇧M でメインウィンドウを表示できます", systemImage: "keyboard") .font(.caption) .foregroundColor(.secondary) } .padding() .background(Color.blue.opacity(0.1)) .cornerRadius(8) } Divider() // サンプルコンテンツ VStack(alignment: .leading, spacing: 12) { Text("アプリの機能") .font(.headline) ForEach(1...5, id: \.self) { index in HStack { Image(systemName: "\(index).circle.fill") .foregroundColor(.blue) Text("機能 \(index)") } } } .frame(maxWidth: .infinity, alignment: .leading) Spacer() } .padding() .frame(width: 500, height: 400) } } // メニューバーから開くポップオーバーのコンテンツ struct MenuBarContentView: View { @Bindable var menuBarManager: MenuBarManager var body: some View { VStack(spacing: 16) { HStack { Text("メニューバーアプリ") .font(.headline) Spacer() Button(action: { menuBarManager.showMainWindow() }) { Image(systemName: "arrow.up.left.and.arrow.down.right") } .buttonStyle(.plain) .help("メインウィンドウを開く") } Divider() ScrollView { VStack(alignment: .leading, spacing: 12) { Text("クイックアクション") .font(.subheadline) .fontWeight(.semibold) ForEach(1...10, id: \.self) { index in Button(action: { print("アクション \(index) が実行されました") }) { HStack { Image(systemName: "star.fill") .foregroundColor(.yellow) Text("アクション \(index)") .foregroundColor(.primary) Spacer() } .padding(8) .background(Color.gray.opacity(0.1)) .cornerRadius(6) } .buttonStyle(.plain) } } } Divider() HStack { Toggle("メニューバーモード", isOn: $menuBarManager.isMenuBarMode) .toggleStyle(.switch) .controlSize(.small) } Button("終了") { NSApplication.shared.terminate(nil) } .foregroundColor(.red) } .padding() .frame(width: 400, height: 500) } } #Preview { ContentView(menuBarManager: MenuBarManager()) } |
例2:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 |
import SwiftUI import AppKit // メニューバー管理クラス @Observable class MenuBarManager { var isMenuBarMode: Bool { didSet { UserDefaults.standard.set(isMenuBarMode, forKey: "isMenuBarMode") updateMenuBarStatus() } } private var statusItem: NSStatusItem? private var popover: NSPopover? private var isInitialized = false init() { // 保存された設定を読み込み(この時点では適用しない) self.isMenuBarMode = UserDefaults.standard.bool(forKey: "isMenuBarMode") } // アプリ起動後に呼び出す初期化メソッド func initialize() { guard !isInitialized else { return } isInitialized = true if isMenuBarMode { // 少し遅延させてウィンドウが作成されるのを待つ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in self?.updateMenuBarStatus() } } } private func setupMenuBar() { // ステータスバーアイテムを作成 statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) if let button = statusItem?.button { button.image = NSImage(systemSymbolName: "app.fill", accessibilityDescription: "アプリ") button.action = #selector(togglePopover) button.target = self } // ポップオーバーを作成 popover = NSPopover() popover?.contentSize = NSSize(width: 400, height: 500) popover?.behavior = .transient popover?.contentViewController = NSHostingController(rootView: MenuBarContentView(menuBarManager: self)) } private func removeMenuBar() { if let statusItem = statusItem { NSStatusBar.system.removeStatusItem(statusItem) self.statusItem = nil } popover = nil } @objc private func togglePopover() { guard let button = statusItem?.button else { return } if let popover = popover { if popover.isShown { popover.performClose(nil) } else { popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY) } } } private func updateMenuBarStatus() { if isMenuBarMode { setupMenuBar() // メインウィンドウを隠す NSApp.windows.first?.orderOut(nil) } else { removeMenuBar() // メインウィンドウを表示 if let window = NSApp.windows.first { window.makeKeyAndOrderFront(nil) NSApp.activate(ignoringOtherApps: true) } } } func showMainWindow() { isMenuBarMode = false } } // メインアプリ @main struct MenuBarApp: App { @State private var menuBarManager = MenuBarManager() var body: some Scene { WindowGroup { ContentView(menuBarManager: menuBarManager) } .commands { // メニューバーのコマンド追加 CommandGroup(after: .appInfo) { Button("メニューバーから開く") { menuBarManager.showMainWindow() } .keyboardShortcut("m", modifiers: [.command, .shift]) } } } } // メインビュー struct ContentView: View { @Bindable var menuBarManager: MenuBarManager var body: some View { VStack(spacing: 20) { Text("メニューバー常駐アプリ") .font(.title) Divider() Toggle("メニューバーに常駐", isOn: $menuBarManager.isMenuBarMode) .toggleStyle(.switch) if menuBarManager.isMenuBarMode { VStack(alignment: .leading, spacing: 8) { Label("メニューバーのアイコンをクリックしてアプリを開けます", systemImage: "info.circle") .font(.caption) .foregroundColor(.secondary) Label("⌘⇧M でメインウィンドウを表示できます", systemImage: "keyboard") .font(.caption) .foregroundColor(.secondary) } .padding() .background(Color.blue.opacity(0.1)) .cornerRadius(8) } Divider() // サンプルコンテンツ VStack(alignment: .leading, spacing: 12) { Text("アプリの機能") .font(.headline) ForEach(1...5, id: \.self) { index in HStack { Image(systemName: "\(index).circle.fill") .foregroundColor(.blue) Text("機能 \(index)") } } } .frame(maxWidth: .infinity, alignment: .leading) Spacer() } .padding() .frame(width: 500, height: 400) .onAppear { // ビューが表示されたタイミングで初期化 menuBarManager.initialize() } } } // メニューバーから開くポップオーバーのコンテンツ struct MenuBarContentView: View { @Bindable var menuBarManager: MenuBarManager var body: some View { VStack(spacing: 16) { HStack { Text("メニューバーアプリ") .font(.headline) Spacer() Button(action: { menuBarManager.showMainWindow() }) { Image(systemName: "arrow.up.left.and.arrow.down.right") } .buttonStyle(.plain) .help("メインウィンドウを開く") } Divider() ScrollView { VStack(alignment: .leading, spacing: 12) { Text("クイックアクション") .font(.subheadline) .fontWeight(.semibold) ForEach(1...10, id: \.self) { index in Button(action: { print("アクション \(index) が実行されました") }) { HStack { Image(systemName: "star.fill") .foregroundColor(.yellow) Text("アクション \(index)") .foregroundColor(.primary) Spacer() } .padding(8) .background(Color.gray.opacity(0.1)) .cornerRadius(6) } .buttonStyle(.plain) } } } Divider() HStack { Toggle("メニューバーモード", isOn: $menuBarManager.isMenuBarMode) .toggleStyle(.switch) .controlSize(.small) } Button("終了") { NSApplication.shared.terminate(nil) } .foregroundColor(.red) } .padding() .frame(width: 400, height: 500) } } #Preview { ContentView(menuBarManager: MenuBarManager()) } |
MenuBarContentView のボタンに自分の非表示、再表示させる機能を持たせる
MenuBarContentViewのポップオーバーを閉じるには、NSPopoverの参照が必要です。いくつか方法がありますが、最もシンプルな方法を紹介しますね:
方法1: @Environmentを使う(推奨)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
struct MenuBarContentView: View { @Bindable var menuBarManager: MenuBarManager @Environment(\.dismiss) private var dismiss // ← これを追加 var body: some View { VStack(spacing: 16) { // ... 他のコード Button(action: { print("アクション実行") dismiss() // ← ポップオーバーを閉じる }) { HStack { Image(systemName: "star.fill") Text("アクションを実行して閉じる") Spacer() } } } } } |
方法2: MenuBarManagerにメソッドを追加
より細かい制御が必要な場合は、MenuBarManagerに閉じるメソッドを追加:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
@Observable class MenuBarManager { // ... 既存のコード private var statusItem: NSStatusItem? private var popover: NSPopover? // ポップオーバーを閉じるメソッドを追加 func closePopover() { popover?.performClose(nil) } // ポップオーバーを開くメソッド(既存) @objc private func togglePopover() { // ... 既存のコード } } |
そしてMenuBarContentViewで:
|
1 2 3 4 5 6 |
Button(action: { print("アクション実行") menuBarManager.closePopover() // ← これで閉じる }) { // ボタンの内容 } |
方法3: ポップオーバー自体をEnvironmentで渡す
より高度な制御が必要な場合:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// カスタムEnvironmentKeyを定義 private struct PopoverKey: EnvironmentKey { static let defaultValue: NSPopover? = nil } extension EnvironmentValues { var popover: NSPopover? { get { self[PopoverKey.self] } set { self[PopoverKey.self] = newValue } } } // MenuBarManagerのsetupMenuBar()内で popover?.contentViewController = NSHostingController( rootView: MenuBarContentView(menuBarManager: self) .environment(\.popover, popover) // ← 渡す ) // MenuBarContentView内で @Environment(\.popover) private var popover Button("閉じる") { popover?.performClose(nil) } |
おすすめ
方法1の@Environment(\.dismiss)が最もシンプルで、SwiftUIの標準的なやり方です。ほとんどのケースではこれで十分です!
再表示するには
再表示する方法ですが、実はポップオーバーは自動的に再表示できます!
再表示の方法
ポップオーバーを閉じた後、メニューバーのアイコンをもう一度クリックすれば再表示されます。これはtogglePopover()メソッドで既に実装されています。
もしプログラム的に再表示したい場合は、以下のようにします:
方法1: MenuBarManagerにメソッドを追加
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
@Observable class MenuBarManager { // ... 既存のコード // ポップオーバーを閉じる func closePopover() { popover?.performClose(nil) } // ポップオーバーを開く func openPopover() { guard let button = statusItem?.button else { return } popover?.show(relativeTo: button.bounds, of: button, preferredEdge: .minY) } // ポップオーバーの表示状態をトグル(既存のメソッドと同じ) func togglePopover() { guard let button = statusItem?.button else { return } if let popover = popover { if popover.isShown { popover.performClose(nil) } else { popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY) } } } } |
使用例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
struct MenuBarContentView: View { @Bindable var menuBarManager: MenuBarManager @Environment(\.dismiss) private var dismiss var body: some View { VStack(spacing: 16) { // アクションを実行して閉じるボタン Button(action: { print("アクション実行") dismiss() // 閉じる // 3秒後に再表示 DispatchQueue.main.asyncAfter(deadline: .now() + 3) { menuBarManager.openPopover() } }) { Text("3秒後に再表示") } // 単純に閉じるだけ Button("閉じる") { dismiss() } } } } |
補足: ポップオーバーの表示状態を確認
|
1 2 3 4 |
// MenuBarManagerに追加 var isPopoverShown: Bool { popover?.isShown ?? false } |
これで、ポップオーバーが表示されているかどうかを確認できます。
通常はユーザーがメニューバーアイコンをクリックして再表示するのが自然な動作ですが、特定の処理後に自動で再表示したい場合はopenPopover()メソッドを使ってください!
コメント