LoginSignup
2
3

【SwiftUI】Menu で要素の先頭にチェックマークを付ける

Posted at

はじめに

仕事でこんな感じのデザイン指示がありました。

image.png

ボタンをタップすると選択されている要素の先頭に✅がついているメニューが表示されるというもの。

少し手こずったので実装方法を書き残します。

忙しい人向けの概要

※ 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.png

何故か Image が右端に位置しています…。
HStack は上から定義した順に 左 → 右 に配置されるはずです。

このボタンを Menu から出してみるとこんな感じです。

image.png

ちゃんと 左 → 右 に配置されています :thinking:

書き方はおかしくないということなので、
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.

アクションを表すシンボルを表示しろと書いていますね。
そして右端に画像を配置している図もあります。

image.png

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)
                }
            }
        )
    }
}

プレビューで見てみます。

image.png

欲しかったやつですね!!!

ただ、表示元のボタンが 「選択中の要素+↕️」 になってしまっています。
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)
        }
    }
}

プレビューで見てみます。

image.png

完璧ですね🎉🎉

先ほど Menu は対象に作用する機能をまとめたものであり、「要素を一覧化して選択状態を示す機能」は Menu の定義と少し離れていると書きましたが、「一覧の中からどれか一つを選んで対象に適用する機能」と考えれば Picker は Menu のアクションの一つと考えることができます。

ちなみに Menu の中に Button、Menu、Picker を全て配置するとこんな感じになります。

image.png

Picker の4行がボタンの1行と同等ということですね。(それを表すためにセパレーターが表示されているのだと思います。)

最後に

Menu で要素の先頭にチェックマークを付ける方法、検索しても意外とヒットしませんでした。(特に日本語だと)
Menu の公式ドキュメントに何が使えるか列挙してくれていれば苦労しないのに :sob:

というわけで、出来そうなのに出来ない…となっている人の役に立てば幸いです :pray:

【余談】この記事を書きながら思ったこと

SwiftUI で実装していると、HIG に則っていない形式を排除したいという🍎さんの意図を感じることがあります。

UIKit と比べると不便だなと思うことが多いですが、HIG 準拠への強制力は個人的にはめちゃくちゃ好きです。

とりあえず標準コンポーネントを使っていればはちゃめちゃなデザインにはならないので、アプリ品質を底上げしてくれます。

ありがとう🍎!

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3