はじめに
仕事でこんな感じのデザイン指示がありました。
ボタンをタップすると選択されている要素の先頭に✅がついているメニューが表示されるというもの。
少し手こずったので実装方法を書き残します。
忙しい人向けの概要
※ HIG の考察など寄り道をしているので実装方法だけ知りたい人はリンクからどうぞ。
Menu で実装してみる
まずは素直に Menu で実装してみます。
struct MenuView: View {
enum Season: String, CaseIterable {
case spring = "🌸"
case summer = "🌻"
case autumn = "🍂"
case winter = "⛄️"
}
@State var selectedSeason: Season = .spring
var body: some View {
Menu {
ForEach(Season.allCases, id: \.self) { season in
Button {
selectedSeason = season
} label: {
HStack {
if selectedSeason == season {
Image(systemName: "checkmark")
}
Text(season.rawValue)
}
}
}
} label: {
Image(systemName: "ellipsis")
.foregroundStyle(.black)
}
}
}
Menu に表示する行の定義は下記の部分。
HStack でチェックマーク✅とテキストを横積みにしています。
Button {
selectedSeason = season
} label: {
HStack {
if selectedSeason == season {
Image(systemName: "checkmark")
}
Text(season.rawValue)
}
}
プレビューで見てみます。
何故か Image が右端に位置しています…。
HStack は上から定義した順に 左 → 右
に配置されるはずです。
このボタンを Menu から出してみるとこんな感じです。
ちゃんと 左 → 右
に配置されています
書き方はおかしくないということなので、
Menu が勝手に Image を右端に配置しているということですね。。。
HIG から Menu の定義に立ち返る
なぜ Menu がそんなことをしてしまうのか、疑問に思いますよね。
こういう時は HIG を見てみます。
Menu のページの先頭にこう書いてありました。
A menu reveals its options when people interact with it, making it a space-efficient way to present commands in your app or game.
つまり Menu は対象に作用する操作・機能をまとめたコンポーネントなのです。
さらにこうも書かれています。
For each action, use a recognizable symbol that helps people identify the action without a label.
アクションを表すシンボルを表示しろと書いていますね。
そして右端に画像を配置している図もあります。
SwifUI は「Menu に含まれる Image = アクションを表すシンボル」という解釈で、Image を勝手に右端に配置しているようです。
というわけで、HIG を読む限り今回のやりたいことである「要素を一覧化して選択状態を示す」機能は Menu の定義と少し離れているように感じます。
Picker で実装してみる
Menu の主旨ではないとしても、選択状態を表すメニュー表示のコンポーネントはよく見かけますよね。
あれは Menu ではなく Picker なのです。
Picker で実装してみます。
struct PickerView: View {
enum Season: String, CaseIterable {
case spring = "🌸"
case summer = "🌻"
case autumn = "🍂"
case winter = "⛄️"
}
@State var selectedSeason: Season = .spring
var body: some View {
Picker(
// この String は pickerStyle: .navigationLink の時に表示されるラベル
"季節",
selection: $selectedSeason,
content: {
ForEach(Season.allCases, id: \.self) { season in
Text(season.rawValue)
.tag(season)
}
}
)
}
}
プレビューで見てみます。
欲しかったやつですね!!!
ただ、表示元のボタンが 「選択中の要素+↕️」
になってしまっています。
pickerStyle を変えてみたり、別の init を使ってみても、ボタンのラベルを変えることはできません…。
痒いところに手が届かないのが SwiftUI って感じですよね…。
Menu と Picker を合体してみる
実は Menu と Picker を併用することで実現可能です。
Menu は Button 意外にも Menu や Picker を内部に含むことができます。
では実際に Menu の中に Picker を含めてみます。
struct PickerInMenuView: View {
enum Season: String, CaseIterable {
case spring = "🌸"
case summer = "🌻"
case autumn = "🍂"
case winter = "⛄️"
}
@State var selectedSeason: Season = .spring
var body: some View {
Menu {
Picker(
"季節",
selection: $selectedSeason,
content: {
ForEach(Season.allCases, id: \.self) { season in
Text(season.rawValue)
.tag(season)
}
}
)
} label: {
Image(systemName: "ellipsis")
.foregroundStyle(.black)
}
}
}
プレビューで見てみます。
完璧ですね🎉🎉
先ほど Menu は対象に作用する機能をまとめたものであり、「要素を一覧化して選択状態を示す機能」は Menu の定義と少し離れていると書きましたが、「一覧の中からどれか一つを選んで対象に適用する機能」と考えれば Picker は Menu のアクションの一つと考えることができます。
ちなみに Menu の中に Button、Menu、Picker を全て配置するとこんな感じになります。
Picker の4行がボタンの1行と同等ということですね。(それを表すためにセパレーターが表示されているのだと思います。)
最後に
Menu で要素の先頭にチェックマークを付ける方法、検索しても意外とヒットしませんでした。(特に日本語だと)
Menu の公式ドキュメントに何が使えるか列挙してくれていれば苦労しないのに
というわけで、出来そうなのに出来ない…となっている人の役に立てば幸いです
【余談】この記事を書きながら思ったこと
SwiftUI で実装していると、HIG に則っていない形式を排除したいという🍎さんの意図を感じることがあります。
UIKit と比べると不便だなと思うことが多いですが、HIG 準拠への強制力は個人的にはめちゃくちゃ好きです。
とりあえず標準コンポーネントを使っていればはちゃめちゃなデザインにはならないので、アプリ品質を底上げしてくれます。
ありがとう🍎!