iPhoneアプリでよく見かける「…」マーク(三点リーダー)をタップすると、編集・共有・削除などのオプションが表示されますよね。このようなドロップダウンメニューを簡単に実装できるのが、SwiftUIのMenu
です。
本記事では、Swift初心者の方でも理解できるように、Menu
の基本から実践的な使い方まで詳しく解説します。アプリに洗練されたメニュー機能を追加したい方は、ぜひ最後までお読みください。
Menuとは?
Menu
は、SwiftUIでドロップダウンメニューやオプションメニューを作成するためのコンポーネントです。iOS 14以降で利用でき、ユーザーがタップすると複数の選択肢が表示されます。
Menuの主な特徴
- iOS 14以降で利用可能
- タップで表示されるドロップダウンメニュー
- 階層構造(サブメニュー)に対応
- アイコンとテキストを組み合わせ可能
- 破壊的なアクション(削除など)の表現が可能
Menuが使われる場面
- ツールバーのオプションメニュー
- リスト各行のアクションボタン
- 編集・削除・共有などの操作選択
- フィルターや並び替えの選択
- 設定やオプションの切り替え
基本的な使い方
最もシンプルなMenu
まずは、最も基本的なMenu
の作り方から見ていきましょう。
import SwiftUI
struct ContentView: View {
var body: some View {
Menu("メニュー") {
Button("オプション1") {
print("オプション1が選択されました")
}
Button("オプション2") {
print("オプション2が選択されました")
}
Button("オプション3") {
print("オプション3が選択されました")
}
}
}
}
このコードだけで、タップすると3つの選択肢が表示されるメニューが完成します。
コードの解説
Menu("メニュー")
: メニューボタンのラベルテキストButton
: メニュー内の各選択肢{}
: ボタンがタップされたときの処理
アイコン付きMenuの作成
アイコンを追加すると、より直感的なメニューになります。
struct ContentView: View {
var body: some View {
Menu {
Button(action: {
print("編集")
}) {
Label("編集", systemImage: "pencil")
}
Button(action: {
print("共有")
}) {
Label("共有", systemImage: "square.and.arrow.up")
}
Button(action: {
print("削除")
}) {
Label("削除", systemImage: "trash")
}
} label: {
Image(systemName: "ellipsis.circle")
.font(.title2)
}
}
}
Labelの使い方
Label
を使うと、アイコンとテキストを簡単に組み合わせられます。
- 第1引数: 表示するテキスト
systemImage
: SF Symbolsのアイコン名
カスタムラベルのMenu
メニューボタンのデザインを自由にカスタマイズできます。
struct ContentView: View {
var body: some View {
Menu {
Button("コピー") {
print("コピー")
}
Button("貼り付け") {
print("貼り付け")
}
Button("切り取り") {
print("切り取り")
}
} label: {
HStack {
Image(systemName: "doc.on.doc")
Text("編集")
}
.padding(.horizontal, 20)
.padding(.vertical, 10)
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
}
}
}
このように、label
クロージャ内で自由にデザインを定義できます。
階層化されたMenu(サブメニュー)
メニューの中にさらにメニューを作成することができます。
struct ContentView: View {
var body: some View {
Menu("ファイル") {
Button("新規作成") {
print("新規作成")
}
Button("開く") {
print("開く")
}
Menu("最近使用したファイル") {
Button("document1.txt") {
print("document1.txt")
}
Button("document2.txt") {
print("document2.txt")
}
Button("document3.txt") {
print("document3.txt")
}
}
Button("保存") {
print("保存")
}
}
.font(.title2)
}
}
サブメニューは最大3階層まで作成できますが、ユーザビリティを考えると2階層までが推奨されます。
Divider(区切り線)の使用
関連する項目をグループ化するために、区切り線を追加できます。
struct ContentView: View {
var body: some View {
Menu("オプション") {
Button(action: {}) {
Label("編集", systemImage: "pencil")
}
Button(action: {}) {
Label("コピー", systemImage: "doc.on.doc")
}
Divider()
Button(action: {}) {
Label("共有", systemImage: "square.and.arrow.up")
}
Divider()
Button(action: {}) {
Label("削除", systemImage: "trash")
}
} label: {
Image(systemName: "ellipsis.circle")
}
}
}
Divider()
を使うことで、視覚的に項目を整理できます。
破壊的なアクション(削除など)
削除のような重要なアクションは、赤色で表示して注意を促すことができます。
struct ContentView: View {
var body: some View {
Menu("アクション") {
Button(action: {}) {
Label("編集", systemImage: "pencil")
}
Button(action: {}) {
Label("共有", systemImage: "square.and.arrow.up")
}
Divider()
Button(role: .destructive) {
print("削除")
} label: {
Label("削除", systemImage: "trash")
}
} label: {
Image(systemName: "ellipsis.circle")
}
}
}
role: .destructive
を指定すると、そのボタンが自動的に赤色で表示されます。
Sectionでグループ化
iOS 15以降では、Section
を使ってメニュー項目をグループ化できます。
struct ContentView: View {
var body: some View {
Menu("メニュー") {
Section("編集") {
Button(action: {}) {
Label("コピー", systemImage: "doc.on.doc")
}
Button(action: {}) {
Label("貼り付け", systemImage: "doc.on.clipboard")
}
}
Section("操作") {
Button(action: {}) {
Label("共有", systemImage: "square.and.arrow.up")
}
Button(action: {}) {
Label("エクスポート", systemImage: "arrow.up.doc")
}
}
} label: {
Image(systemName: "ellipsis.circle")
}
}
}
Toolbarでの使用
ナビゲーションバーにメニューを配置する方法です。
struct ContentView: View {
var body: some View {
NavigationView {
Text("メインコンテンツ")
.navigationTitle("タイトル")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Menu {
Button(action: {}) {
Label("設定", systemImage: "gear")
}
Button(action: {}) {
Label("ヘルプ", systemImage: "questionmark.circle")
}
Divider()
Button(action: {}) {
Label("ログアウト", systemImage: "arrow.right.square")
}
} label: {
Image(systemName: "ellipsis.circle")
}
}
}
}
}
}
この実装は、多くのアプリで見られる標準的なパターンです。
リストの各行にMenuを追加
リストの各アイテムにメニューを追加する実装例です。
struct Item: Identifiable {
let id = UUID()
let name: String
}
struct ContentView: View {
let items = [
Item(name: "アイテム1"),
Item(name: "アイテム2"),
Item(name: "アイテム3")
]
var body: some View {
NavigationView {
List(items) { item in
HStack {
Text(item.name)
Spacer()
Menu {
Button(action: {}) {
Label("編集", systemImage: "pencil")
}
Button(action: {}) {
Label("共有", systemImage: "square.and.arrow.up")
}
Divider()
Button(role: .destructive) {
print("\(item.name)を削除")
} label: {
Label("削除", systemImage: "trash")
}
} label: {
Image(systemName: "ellipsis")
.foregroundColor(.gray)
}
}
}
.navigationTitle("リスト")
}
}
}
Pickerとの組み合わせ
メニュー内で選択肢を切り替える実装です。
struct ContentView: View {
@State private var selectedSortOrder = "名前順"
let sortOrders = ["名前順", "日付順", "サイズ順"]
var body: some View {
VStack(spacing: 20) {
Text("現在の並び順: \(selectedSortOrder)")
.font(.headline)
Menu {
Picker("並び順", selection: $selectedSortOrder) {
ForEach(sortOrders, id: \.self) { order in
Text(order).tag(order)
}
}
} label: {
HStack {
Text("並び順を変更")
Image(systemName: "chevron.down")
}
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
}
}
}
}
Picker
を使うことで、現在選択されている項目にチェックマークが表示されます。
実践的な画像編集メニュー
複数の機能を持つ実用的なメニューの例です。
struct ImageEditorView: View {
@State private var selectedFilter = "オリジナル"
@State private var showingDeleteAlert = false
var body: some View {
VStack(spacing: 30) {
Image(systemName: "photo")
.resizable()
.scaledToFit()
.frame(height: 200)
.foregroundColor(.blue)
Text("選択中のフィルター: \(selectedFilter)")
.font(.subheadline)
.foregroundColor(.gray)
Menu {
Section("編集") {
Button(action: {
print("トリミング")
}) {
Label("トリミング", systemImage: "crop")
}
Button(action: {
print("回転")
}) {
Label("回転", systemImage: "rotate.right")
}
Button(action: {
print("反転")
}) {
Label("反転", systemImage: "arrow.left.and.right")
}
}
Section("フィルター") {
Button("オリジナル") {
selectedFilter = "オリジナル"
}
Button("モノクロ") {
selectedFilter = "モノクロ"
}
Button("セピア") {
selectedFilter = "セピア"
}
Button("ビンテージ") {
selectedFilter = "ビンテージ"
}
}
Section {
Button(role: .destructive) {
showingDeleteAlert = true
} label: {
Label("削除", systemImage: "trash")
}
}
} label: {
Label("編集メニュー", systemImage: "slider.horizontal.3")
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
.alert("画像を削除しますか?", isPresented: $showingDeleteAlert) {
Button("キャンセル", role: .cancel) {}
Button("削除", role: .destructive) {
print("画像を削除")
}
}
}
.padding()
}
}
この実装では、編集機能、フィルター選択、削除機能を1つのメニューにまとめています。
タスク管理アプリでの実装例
実践的なタスク管理アプリでのメニュー活用例です。
import SwiftUI
struct Task: Identifiable {
let id = UUID()
var title: String
var priority: Priority
var isCompleted: Bool
enum Priority: String, CaseIterable {
case high = "高"
case medium = "中"
case low = "低"
var color: Color {
switch self {
case .high: return .red
case .medium: return .orange
case .low: return .blue
}
}
}
}
struct TaskListView: View {
@State private var tasks = [
Task(title: "レポートを書く", priority: .high, isCompleted: false),
Task(title: "買い物に行く", priority: .medium, isCompleted: false),
Task(title: "メールを返信する", priority: .high, isCompleted: true),
Task(title: "ジムに行く", priority: .low, isCompleted: false)
]
var body: some View {
NavigationView {
List {
ForEach($tasks) { $task in
HStack(spacing: 12) {
Button {
task.isCompleted.toggle()
} label: {
Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
.foregroundColor(task.isCompleted ? .green : .gray)
.font(.title3)
}
.buttonStyle(.plain)
VStack(alignment: .leading, spacing: 4) {
Text(task.title)
.strikethrough(task.isCompleted)
.foregroundColor(task.isCompleted ? .gray : .primary)
Text("優先度: \(task.priority.rawValue)")
.font(.caption)
.foregroundColor(task.priority.color)
}
Spacer()
Menu {
Menu("優先度を変更") {
ForEach(Task.Priority.allCases, id: \.self) { priority in
Button(priority.rawValue) {
task.priority = priority
}
}
}
Button {
print("編集: \(task.title)")
} label: {
Label("編集", systemImage: "pencil")
}
Button {
duplicateTask(task)
} label: {
Label("複製", systemImage: "doc.on.doc")
}
Divider()
Button(role: .destructive) {
deleteTask(task)
} label: {
Label("削除", systemImage: "trash")
}
} label: {
Image(systemName: "ellipsis")
.foregroundColor(.gray)
.font(.title3)
}
}
.padding(.vertical, 4)
}
}
.navigationTitle("タスク")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Menu {
Button(action: {}) {
Label("すべて完了にする", systemImage: "checkmark.circle")
}
Button(action: {}) {
Label("完了済みを削除", systemImage: "trash")
}
Divider()
Menu("並び替え") {
Button("優先度順") {}
Button("作成日順") {}
Button("タイトル順") {}
}
} label: {
Image(systemName: "ellipsis.circle")
}
}
}
}
}
private func duplicateTask(_ task: Task) {
let newTask = Task(title: "\(task.title)のコピー", priority: task.priority, isCompleted: false)
tasks.append(newTask)
}
private func deleteTask(_ task: Task) {
tasks.removeAll { $0.id == task.id }
}
}
この実装では、以下の機能を実現しています。
- 各タスクに優先度変更、編集、複製、削除のメニュー
- ツールバーに一括操作と並び替えのメニュー
- 階層化されたサブメニュー
- 破壊的なアクションの視覚的な区別
MenuとcontextMenuの違い
SwiftUIにはMenu
と似た機能のcontextMenu
があります。使い分けを理解しましょう。
Menuの特徴
// 通常のタップで表示
Menu("メニュー") {
Button("オプション1") {}
Button("オプション2") {}
}
- 通常のタップで表示される
- ボタンとして視覚的に認識しやすい
- ツールバーやボタンとして配置
contextMenuの特徴
// 長押しで表示
Text("長押ししてください")
.contextMenu {
Button("コピー") {}
Button("共有") {}
Button("削除") {}
}
- 長押しで表示される
- ビュー自体にメニューが付属
- 隠れた機能として実装
使い分けのポイント
Menuを使うべき場合:
- メニューの存在を明示的に示したい
- ツールバーやナビゲーションバーに配置
- 頻繁に使う機能
contextMenuを使うべき場合:
- 追加機能として控えめに提供
- テキストや画像に操作を追加
- iOSの標準的な長押し操作に準拠
よくある質問(FAQ)
Q1. メニューが表示されない
iOS 14未満ではMenu
が使えません。iOSのバージョンを確認してください。
// デプロイメントターゲットをiOS 14以上に設定
Q2. メニューを閉じるボタンを追加したい
メニューは自動的に閉じるため、閉じるボタンは不要です。外側をタップするか、選択肢をタップすると自動的に閉じます。
Q3. メニュー内のボタンが無効化されない
disabled
モディファイアを使います。
Menu("メニュー") {
Button("有効なボタン") {}
Button("無効なボタン") {}
.disabled(true)
}
Q4. メニューの表示位置を変更したい
メニューの表示位置は自動的に最適化されるため、手動で変更することはできません。
Q5. メニューのアニメーションをカスタマイズしたい
メニューのアニメーションはシステムが管理しているため、カスタマイズはできません。
ベストプラクティス
わかりやすいラベルを使う
// ✅ 良い例:アイコンとテキストで明確に
Menu {
Button(action: {}) {
Label("編集", systemImage: "pencil")
}
}
// ❌ 悪い例:テキストだけ
Menu {
Button("編集") {}
}
破壊的なアクションは明確に
// ✅ 良い例:roleを指定して区切る
Menu("アクション") {
Button("編集") {}
Button("共有") {}
Divider()
Button(role: .destructive) {
// 削除処理
} label: {
Label("削除", systemImage: "trash")
}
}
メニュー項目は5〜7個まで
// ✅ 良い例:項目数が適切
Menu("メニュー") {
Button("オプション1") {}
Button("オプション2") {}
Button("オプション3") {}
Button("オプション4") {}
Button("オプション5") {}
}
// ❌ 悪い例:項目が多すぎる
Menu("メニュー") {
// 10個以上のボタン...
}
項目が多い場合は、サブメニューやSectionで整理しましょう。
階層は2階層まで
// ✅ 良い例:2階層まで
Menu("ファイル") {
Button("新規作成") {}
Menu("最近使用したファイル") {
Button("file1.txt") {}
}
}
// ❌ 悪い例:3階層以上
Menu("ファイル") {
Menu("サブメニュー1") {
Menu("サブメニュー2") {
// 深すぎる...
}
}
}
まとめ
Menu
を使えば、以下のような機能を簡単に実装できます。
- タップで表示されるドロップダウンメニュー
- アイコン付きの直感的な選択肢
- 階層化されたサブメニュー
- 破壊的なアクションの明確な表現
- ツールバーやリストへの統合
初心者の方は、まずシンプルなテキストメニューから始めて、徐々にアイコンやサブメニューを追加していくことをおすすめします。
Menu
は、アプリに洗練された操作性を追加する重要なコンポーネントです。ぜひ今回学んだ内容を活かして、ユーザーフレンドリーなアプリを作ってみてください!