SwiftUIでPickerとMenuPickerStyleを使ったドロップダウンリスト

SwiftUI側にバグがあるのかどうかわかりませんが、文字列のみのドロップダウンメニューならば以下の方法でできるようです。

iOS 14 menu picker style’s label becomes dim sometimes when changing

ただ、前提として中身の項目が文字列限定だったり、項目をユニーク(一意)にしないと選択された項目を特定できないシナリオもあるためIndex的なのを使いたい、と。

そこで、Pickerに表示させる項目をまとめるclassをObservableObjectを指定して作って、監視対象にする方法でこれを実現するところに辿り着きました。

SwiftUIの場合、選択したアイテムをViewに反映させるため、ObservableObject(以下のコードのBookShelfクラス)のプロパティとして表示させる項目を@Publishedにした上で、Pickerの表示項目として指定します。

実際の選択部分にはObservableObject内のindex的なメンバ(以下のselectedIndex)に “didSet” を追加指定して変更が発生した際に表示させる項目(以下の@PublishedなselectedBook)にも変更を加えることで@Published経由でUIへ選択項目を反映させます。

Picker内で一覧として指定する項目(例ではStringのArrayなbooks)は、ObservableObjectのinit()時に構築してもよいですし、Picker表示前であればObservableObjectに追加用のfuncなどを自前で用意して構築してもよいかと思われます。

import SwiftUI

class BookShelf: ObservableObject {
    var books: [String] = []
    @Published var selectedBook: String = ""
    var selectedIndex: Int = 0 {
        didSet {
            self.selectedBook = self.books[self.selectedIndex]
            print("index[\(self.selectedIndex)]: \(self.selectedBook)") // Pickerの選択を変更するたびにDebug areaに選択項目を表示して確認します
        }
    }
    init() {
        let array: [String] = ["Hello, World.", "Hungry Caterpillar", "One Hundred and One Dalmatians"]
        for item in array {
            self.books.append(item)
            if self.selectedBook == "" {
                self.selectedBook = item // 最初に選択された項目として指定しておく
            }
        }
    }
}

struct ContentView: View {
    @ObservedObject var bookShelf: BookShelf = BookShelf()
    var body: some View {
        Picker(bookShelf.selectedBook, selection: self.$bookShelf.selectedIndex) {
            ForEach(0..<bookShelf.books.count) { index in
                if let name = bookShelf.books[index] {
                    Text(name).tag(name)
                }
            }
        }
        .border(Color.blue) // この内容だと選択する項目に合わせて自動でサイズが変更されます(文字数に応じて拡大縮小される)
        .pickerStyle(MenuPickerStyle())
    }
}

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

実行時はこんな感じになります。