[SwiftUI] 階層的プルダウンメニューを作る方法 (menu と Picker の比較から)

SwiftUI

Picker の代替にも使える Menu を説明します。

環境&対象

以下の環境で動作確認を行なっています。

  • macOS Big Sur 11.3
  • Xcode 12.5

# iOS での使用と似ているとおもいますが、iOS では確認していません。

Picker

SwiftUI には複数から選択するための UI として Picker が用意されています。

Picker 使用例

以下が、Picker を使った簡単な例です。選択肢 0,1,2,3,4 から選択された情報が、selection0 に入ります。


//
//  ContentView.swift
//
//  Created by : Tomoaki Yagishita on 2021/05/17
//  © 2021  SmallDeskSoftware
//

import SwiftUI

struct ContentView: View {
    let data = [0,1,2,3,4]
    @State var selection0:Int = 0
    var body: some View {
        VStack {
            HStack {
                Picker(selection: $selection0, label: Text("Picker"), content: {
                    ForEach(data, id:\.self) { value in
                        Text("\(value)")
                            .tag(value)
                    }
                })
            }
        }
        .padding(50)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
PickerExample
PickerExample

しかし、Picker では、階層的な選択を実現することができません。

階層的な選択

選択肢によっては以下のような 階層的に選択できる UI が必要となります。

HierarchyMenu
HierarchyMenu

このような選択を Picker で行う場合には、以下のようになってしまいます。

動的に増える Picker による選択

動的に、Picker を増やすことで 階層的に選択できるようにしてみました。

コードは以下のようなコードになります。


//
//  ContentView.swift
//
//  Created by : Tomoaki Yagishita on 2021/05/17
//  © 2021  SmallDeskSoftware
//

import SwiftUI

struct ContentView: View {
    let data = [0,1,2,3,4]
    let option = [[0,1],[2,3,4],[5,6],[7,8,9], [10,11,12,13,14,15]]
    @State var selection0:Int = -1
    @State var selection1:Int = 0
    @State var optionVisible = false
    var body: some View {
        VStack {
            HStack {
                Picker(selection: $selection0, label: Text("Picker"), content: {
                    ForEach(data, id:\.self) { value in
                        Text("\(value)")
                            .tag(value)
                    }
                })
                if optionVisible {
                    // (1)
                    Picker(selection: $selection1, label: Text("Option"), content: {
                        ForEach(option[selection0], id:\.self) { value in
                            Text("\(value)")
                                .tag(value)
                        }
                    })
                }
            }
            .onChange(of: selection0, perform: { value in
                // (2)
                if selection0 != 3 {
                    optionVisible = true
                    selection1 = option[selection0][0]
                } else {
                    optionVisible = false
                }
            })
        }
        .onAppear {
            selection0 = 0
        }
        .padding(49)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
コード解説
  1. 2つめの Picker を用意し、optionVisible 変数で表示を制御します
  2. onChange で選択肢の変更をチェックし、optionVisible を適切な値に変更します

ケースによるんでしょうけど、階層的メニューを期待しているとすこし残念な UI な気がします。

Menu

このようなケースで、Menu を使用することでも 階層的な選択肢を表示することが可能となります。

シンプルな Menu

まずは、シンプルに使用したメニューです。


//
//  ContentView.swift
//
//  Created by : Tomoaki Yagishita on 2021/05/17
//  © 2021  SmallDeskSoftware
//

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            Menu {
                Button(action: {
                  // do something
                }, label: {
                    Text("Item1")
                })
                Button(action: {
                  // do something
                }, label: {
                    Text("Item2")
                })
            } label: {
                Text("Menu")
            }
        .padding(49)
    }
}

Menu では、選択肢をボタンとして追加することで各メニュー選択時の動作を記述することができます。

Picker と Menu の相違点

Picker と Menu の類似点・相違点を挙げてみました。

  • (類似点)Picker / Menu ともに、選択肢から選択させるための UI
  • (類似点)Picker / Menu ともに、プルダウンメニューを作ることができる
  • (相違点)Picker で要素した選択は、タイトルとして表示されるが、Menu では選択された Button の action が実行されるのみ
  • (相違点)Picker での要素選択は、選択要素の持つ .tag 値が selection に設定される。Menu では選択された Button の action が実行されるのみ
  • (相違点)Picker は、配下に Picker を持つことはできない。 Menu は、配下に階層的に Menu を持つことができる。

どちらも プルダウンメニューを作ることができるのですが、上記の相違点に気をつけて 適材適所で使う必要があります。

例えば、階層的プルダウンは、Picker では実現不可なので、 Menu を使用します。

Menu を使う時に注意すべき点

気をつけるべき点として、「Menu は 親 View との依存関係が 他の View とすこし異なる」があります。

うまくいかない例

例えば、Picker の代わりとしての使用を考えると、メニュータイトルを動的に変更することを考えると思います。

このメニュータイトルを @State 変数や @StateObject 変数を使って設定しても、期待通りに更新されません。
具体的には、選択された値が グレイアウトされて表示されてしまいます。

なぜか、再度プルダウンを行うと表示が正しくなります。

MEMO
この振る舞いは、SwiftUI の実装から来ているようです。解消されるかは不明です。

解決策

Menu そのものを切り替えるようにして、強制的に 再構築させるようにします。


//
//  ContentView.swift
//
//  Created by : Tomoaki Yagishita on 2021/05/17
//  © 2021  SmallDeskSoftware
//

import SwiftUI

struct ContentView: View {
    @State var menuTitle:String = "Menu"
    @State var menuToggle = false
    var body: some View {
        VStack {
            if menuToggle {
                menu()
            } else {
                menu()
            }
            
        }
        .padding(49)
    }
    
    func menu() -> some View {
        Menu(menuTitle) {
            Button(action: {
                menuTitle = "Item1"
                menuToggle.toggle()
            }, label: {
                Text("Item1")
            })
            Menu("Optional"){
                Button(action: {
                    menuTitle = "Opt1"
                    menuToggle.toggle()
                }, label: {
                    Text("Opt1")
                })
                Button(action: {
                    menuTitle = "Opt2"
                    menuToggle.toggle()
                }, label: {
                    Text("Opt2")
                })
            }
            Button(action: {
                menuTitle = "Item2"
                menuToggle.toggle()
            }, label: {
                Text("Item2")
            })
        }
    }
    
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

まとめ:Picker と Menu の要点

Picker と Menu の要点
  • Picker は 選択のための専用 UI
  • Menu は 選択から action を実行する UI
  • action で値を設定することで、Menu は Picker 的に使用することができる
  • Menu は、階層的に構築できる
  • Menu の更新タイミングは、癖があるので、気をつける

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です