macOS 15(Sequoia)では、helpButton()モディファイアを使用するのが最も簡単な方法です。以下のようなコードで実装できます:
|
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 |
import SwiftUI struct ContentView: View { var body: some View { VStack(spacing: 20) { HStack { Text("設定項目") Spacer() Button(action: { // ヘルプアクションをここに記述 print("ヘルプがクリックされました") }) { Image(systemName: "questionmark.circle") .font(.system(size: 14)) } .buttonStyle(.plain) .help("この項目についてのヘルプ") } // または、macOS標準のヘルプボタンスタイルを使用 HStack { Text("別の設定項目") Spacer() Button(action: { // ヘルプを表示 }) { Image(systemName: "questionmark.circle") } .buttonStyle(.borderless) .controlSize(.small) .help("詳細情報") } } .padding() .frame(width: 400) } } |
ポイント:
Image(systemName: "questionmark.circle")でシステム標準の「?」アイコンを使用.buttonStyle(.plain)または.borderlessでボタンの余計な装飾を削除.controlSize(.small)で小さいサイズに調整.help()モディファイアでホバー時のツールチップを追加
ポップオーバー
|
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 |
import SwiftUI struct ContentView: View { @State private var showHelp1 = false @State private var showHelp2 = false var body: some View { VStack(spacing: 20) { // 例1: シンプルなヘルプ HStack { Text("Wi-Fi設定") Spacer() Button(action: { showHelp1.toggle() }) { Image(systemName: "questionmark.circle") .font(.system(size: 14)) } .buttonStyle(.plain) .popover(isPresented: $showHelp1, arrowEdge: .trailing) { VStack(alignment: .leading, spacing: 8) { Text("Wi-Fi設定について") .font(.headline) Text("Wi-Fiネットワークへの接続を管理します。利用可能なネットワークから選択して接続できます。") .font(.body) .fixedSize(horizontal: false, vertical: true) } .frame(width: 250) .padding() } } // 例2: より詳細なヘルプ HStack { Text("プライバシー設定") Spacer() Button(action: { showHelp2.toggle() }) { Image(systemName: "questionmark.circle") .font(.system(size: 14)) } .buttonStyle(.plain) .popover(isPresented: $showHelp2, arrowEdge: .trailing) { HelpPopoverView() } } Spacer() } .padding() .frame(width: 500, height: 300) } } // より詳細なヘルプ用のカスタムビュー struct HelpPopoverView: View { var body: some View { VStack(alignment: .leading, spacing: 12) { Text("プライバシー設定") .font(.headline) Text("アプリがアクセスできる情報を管理します。") .font(.body) Divider() VStack(alignment: .leading, spacing: 8) { HelpItem( icon: "location.fill", title: "位置情報", description: "アプリの位置情報へのアクセスを制御" ) HelpItem( icon: "camera.fill", title: "カメラ", description: "カメラへのアクセス許可を管理" ) HelpItem( icon: "mic.fill", title: "マイク", description: "マイクへのアクセス許可を管理" ) } Divider() Link("詳しくはこちら", destination: URL(string: "https://support.apple.com")!) .font(.footnote) } .frame(width: 280) .padding() } } // ヘルプ項目のコンポーネント struct HelpItem: View { let icon: String let title: String let description: String var body: some View { HStack(alignment: .top, spacing: 8) { Image(systemName: icon) .foregroundColor(.blue) .frame(width: 20) VStack(alignment: .leading, spacing: 2) { Text(title) .font(.subheadline) .fontWeight(.medium) Text(description) .font(.caption) .foregroundColor(.secondary) } } } } #Preview { ContentView() } |
主なポイント:
@State変数でポップオーバーの表示状態を管理.popover()モディファイアでポップオーバーを表示arrowEdgeパラメータで矢印の位置を指定(.trailingで右側)- カスタムビューを使って、複雑なヘルプコンテンツも表示可能
macOSのNSPopoverの作り方、SwiftUI版とAppKit版
|
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 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 |
import SwiftUI import AppKit // ======================================== // 方法1: SwiftUIの.popover()(最もシンプル・推奨) // ======================================== struct SimplePopoverExample: View { @State private var showPopover = false var body: some View { Button("ポップオーバーを表示") { showPopover = true } .popover(isPresented: $showPopover) { // ポップオーバーの内容 VStack(spacing: 12) { Text("ポップオーバーの内容") .font(.headline) Text("ここに表示したい内容を配置") Button("閉じる") { showPopover = false } } .padding() .frame(width: 300, height: 200) } } } // ======================================== // 方法2: レイヤー順設定ポップオーバー(実用例) // ======================================== struct LayerSettingsButton: View { @Bindable var TSO: ToolSettingsObject @State private var showPopover = false var body: some View { Button(action: { showPopover.toggle() }) { Label("レイヤー", systemImage: "square.stack.3d.up") } .popover(isPresented: $showPopover, arrowEdge: .bottom) { // ポップオーバーの内容 LayerOrderPopover(TSO: TSO) } } } struct LayerOrderPopover: View { @Bindable var TSO: ToolSettingsObject var body: some View { VStack(alignment: .leading, spacing: 12) { Text("レイヤー順") .font(.headline) Divider() List { ForEach(TSO.layer_order) { layer in HStack { Image(systemName: layer.iconName) .frame(width: 24) Text(layer.displayName) Spacer() } } .onMove { source, destination in TSO.layer_order.move(fromOffsets: source, toOffset: destination) } } .frame(height: 200) Text("ドラッグして並び替え") .font(.caption) .foregroundColor(.secondary) } .padding() .frame(width: 280) } } // ======================================== // 方法3: NSPopoverを直接使う(細かい制御が必要な場合) // ======================================== struct CustomPopoverButton: NSViewRepresentable { let buttonTitle: String let popoverContent: () -> NSView func makeNSView(context: Context) -> NSButton { let button = NSButton(title: buttonTitle, target: context.coordinator, action: #selector(Coordinator.showPopover)) return button } func updateNSView(_ nsView: NSButton, context: Context) { nsView.title = buttonTitle } func makeCoordinator() -> Coordinator { Coordinator(popoverContent: popoverContent) } class Coordinator { let popoverContent: () -> NSView var popover: NSPopover? init(popoverContent: @escaping () -> NSView) { self.popoverContent = popoverContent } @objc func showPopover(_ sender: NSButton) { // ポップオーバーを作成 let popover = NSPopover() // ポップオーバーの設定 popover.contentSize = NSSize(width: 300, height: 200) popover.behavior = .transient // 外側をクリックで閉じる popover.animates = true // コンテンツを設定 let viewController = NSViewController() viewController.view = popoverContent() popover.contentViewController = viewController // ボタンの位置に表示 popover.show(relativeTo: sender.bounds, of: sender, preferredEdge: .maxY) self.popover = popover } } } // 使用例 struct CustomPopoverExample: View { var body: some View { CustomPopoverButton(buttonTitle: "カスタムポップオーバー") { // NSViewを返す let view = NSView(frame: NSRect(x: 0, y: 0, width: 300, height: 200)) view.wantsLayer = true view.layer?.backgroundColor = NSColor.windowBackgroundColor.cgColor let label = NSTextField(labelWithString: "カスタムNSView") label.frame = NSRect(x: 20, y: 80, width: 260, height: 40) label.font = NSFont.systemFont(ofSize: 18) label.alignment = .center view.addSubview(label) return view } } } // ======================================== // 方法4: SwiftUIをNSPopoverで表示 // ======================================== extension NSButton { func showSwiftUIPopover<Content: View>( content: Content, size: NSSize = NSSize(width: 300, height: 200) ) { let popover = NSPopover() popover.contentSize = size popover.behavior = .transient popover.animates = true // SwiftUIビューをホスティング let hostingController = NSHostingController(rootView: content) popover.contentViewController = hostingController popover.show(relativeTo: self.bounds, of: self, preferredEdge: .maxY) } } // 使用例 struct SwiftUIPopoverExample: View { var body: some View { Button("SwiftUIポップオーバー") { // NSButtonを取得する必要がある... } } } // ======================================== // 方法5: ツールバーでの実装例 // ======================================== struct ToolbarWithPopovers: View { @State private var showColorPicker = false @State private var showLayerSettings = false @State private var showFontSettings = false var body: some View { HStack(spacing: 20) { // カラーピッカーポップオーバー Button(action: { showColorPicker.toggle() }) { Label("色", systemImage: "paintpalette") } .popover(isPresented: $showColorPicker) { ColorPickerPopover() } Divider() .frame(height: 30) // レイヤー設定ポップオーバー Button(action: { showLayerSettings.toggle() }) { Label("レイヤー", systemImage: "square.stack.3d.up") } .popover(isPresented: $showLayerSettings, arrowEdge: .bottom) { Text("レイヤー設定") .padding() .frame(width: 250, height: 300) } Divider() .frame(height: 30) // フォント設定ポップオーバー Button(action: { showFontSettings.toggle() }) { Label("フォント", systemImage: "textformat") } .popover(isPresented: $showFontSettings, arrowEdge: .bottom) { FontSettingsPopover() } } .padding() .background(.ultraThinMaterial) } } struct ColorPickerPopover: View { @State private var selectedColor: Color = .red var body: some View { VStack(spacing: 16) { Text("色を選択") .font(.headline) ColorPicker("色", selection: $selectedColor) .labelsHidden() // カスタムカラーパレット LazyVGrid(columns: Array(repeating: GridItem(.fixed(30)), count: 8), spacing: 8) { ForEach([Color.red, .orange, .yellow, .green, .blue, .purple, .pink, .brown, .black, .white, .gray, .cyan, .mint, .indigo, .teal, .clear], id: \.self) { color in Circle() .fill(color) .frame(width: 30, height: 30) .overlay(Circle().stroke(Color.gray, lineWidth: 1)) .onTapGesture { selectedColor = color } } } } .padding() .frame(width: 280) } } struct FontSettingsPopover: View { @State private var fontSize: Double = 14 @State private var isBold = false @State private var isItalic = false var body: some View { VStack(alignment: .leading, spacing: 16) { Text("フォント設定") .font(.headline) Divider() VStack(alignment: .leading) { Text("サイズ: \(Int(fontSize))pt") Slider(value: $fontSize, in: 8...72) } Toggle("太字", isOn: $isBold) Toggle("斜体", isOn: $isItalic) Spacer() } .padding() .frame(width: 220, height: 180) } } // ======================================== // ポップオーバーの動作設定 // ======================================== struct PopoverBehaviorExample: View { @State private var showPopover = false var body: some View { Button("ポップオーバー") { showPopover = true } .popover( isPresented: $showPopover, attachmentAnchor: .point(.bottom), // 矢印の位置 arrowEdge: .bottom // 矢印の向き ) { VStack { Text("ポップオーバー") // ポップオーバーのサイズを明示的に指定 } .padding() .frame(width: 300, height: 200) // 📝 presentationCompactAdaptation でiPad等での動作を制御 .presentationCompactAdaptation(.popover) } } } // ======================================== // 完全な実装例 // ======================================== struct CompleteExample: View { @State private var TSO = ToolSettingsObject() var body: some View { VStack { // メインコンテンツ Text("メインビュー") .frame(maxWidth: .infinity, maxHeight: .infinity) // ツールバー ToolbarWithPopovers() } } } // ToolSettingsObjectのサンプル @Observable class ToolSettingsObject { var layer_order: [CanvasLayerOrder] = [.text, .shape, .drawing, .add_images, .image] } enum CanvasLayerOrder: Int, CaseIterable, Identifiable { case text, shape, drawing, add_images, image var id: Int { rawValue } var displayName: String { switch self { case .text: return "テキスト" case .shape: return "図形" case .drawing: return "ペン描画" case .add_images: return "追加画像" case .image: return "背景画像" } } var iconName: String { switch self { case .text: return "textformat" case .shape: return "square.on.circle" case .drawing: return "pencil" case .add_images: return "photo.stack" case .image: return "photo" } } } |
ポップオーバーのオプション
矢印の向きを指定
|
1 2 3 4 5 6 |
.popover(isPresented: $show, arrowEdge: .bottom) { // 下向き矢印 } // arrowEdge のオプション: // .top, .bottom, .leading, .trailing |
サイズを指定
|
1 2 3 4 5 6 7 |
.popover(isPresented: $show) { VStack { // 内容 } .frame(width: 300, height: 200) // サイズ指定 .padding() } |
実用的な例:複数のポップオーバー
|
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 |
struct ToolbarWithMultiplePopovers: View { @State private var showColors = false @State private var showLayers = false @State private var showFonts = false var body: some View { HStack(spacing: 20) { // カラーピッカー Button("色") { showColors.toggle() } .popover(isPresented: $showColors) { ColorPickerView() .frame(width: 250, height: 300) } // レイヤー設定 Button("レイヤー") { showLayers.toggle() } .popover(isPresented: $showLayers) { LayerSettingsView() .frame(width: 280, height: 250) } // フォント設定 Button("フォント") { showFonts.toggle() } .popover(isPresented: $showFonts) { FontSettingsView() .frame(width: 220, height: 180) } } } } |
ポップオーバーの動作
| 動作 | 説明 |
|---|---|
| 表示 | isPresentedをtrueにする |
| 非表示 | isPresentedをfalseにする |
| 自動で閉じる | 外側をクリックすると自動的に閉じる |
| 矢印 | ボタンを指す矢印が自動的に表示される |
ポイント
- ✅ シンプル:
.popover()修飾子で簡単に実装 - ✅ 自動管理: 位置計算や表示/非表示を自動処理
- ✅ ネイティブ: macOSの標準的な見た目と動作
- ✅ アニメーション: 開閉アニメーションが自動的に適用
コメント