MENU

【SwiftUI】Menuの使い方完全ガイド|ドロップダウンメニューを初心者向けに解説

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は、アプリに洗練された操作性を追加する重要なコンポーネントです。ぜひ今回学んだ内容を活かして、ユーザーフレンドリーなアプリを作ってみてください!

参考リンク

プログラミングの独学におすすめ
プログラミング言語の人気オンラインコース
独学でプログラミングを学習している方で、エラーなどが発生して効率よく勉強ができないと悩む方は多いはず。Udemyは、プロの講師が動画で実際のプログラムを動かしながら教えてくれるオンライン講座です。講座の価格は、セール期間中には専門書籍を1冊買うよりも安く済むことが多いです。新しく学びたいプログラミング言語がある方は、ぜひUdemyでオンライン講座を探してみてください。
目次