Instantly share code, notes, and snippets.
Created
January 4, 2026 07:48
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
-
Save Koshimizu-Takehito/2d849c426b81f62df97dcde35a404aa5 to your computer and use it in GitHub Desktop.
PickerMenuRowForEach/PackedPickerMenuRow
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import SwiftUI | |
| // MARK: - Preview PickerMenuRowForEach | |
| @available(iOS 26.0, *) | |
| #Preview(traits: .assistiveAccess) { | |
| @Previewable @State var values: [Union] = [ | |
| .foo(.foo1), | |
| .bar(.bar1), | |
| .baz(.baz1), | |
| ] | |
| PickerMenuRowForEach($values) { value in | |
| Union.options(for: value) | |
| } | |
| } | |
| // MARK: - Preview PackedPickerMenuRow | |
| @available(iOS 26.0, *) | |
| #Preview(traits: .assistiveAccess) { | |
| @Previewable @State var foo: Foo = .foo1 | |
| @Previewable @State var bar: Bar = .bar1 | |
| @Previewable @State var baz: Baz = .baz1 | |
| PackedPickerMenuRow($foo, $bar, $baz) | |
| } | |
| // MARK: - PickerMenuRowForEach | |
| struct PickerMenuRowForEach<Item: Hashable & Identifiable>: View { | |
| @Binding var values: [Item] | |
| var options: (Item) -> [Item] | |
| init(_ values: Binding<[Item]>, options: @escaping (Item) -> [Item]) { | |
| _values = values | |
| self.options = options | |
| } | |
| var body: some View { | |
| PickerMenuRow { | |
| ForEach($values) { $item in | |
| PickerMenu(selection: $item, options: options(item)) | |
| } | |
| } | |
| } | |
| } | |
| // MARK: - PackedPickerMenuRow | |
| struct PackedPickerMenuRow<each Item: Hashable & Identifiable>: View { | |
| private var selections: (repeat Binding<each Item>) | |
| private var options: (repeat [each Item]) | |
| init(_ selections: repeat Binding<each Item>, options: repeat [each Item]) { | |
| self.selections = (repeat each selections) | |
| self.options = (repeat each options) | |
| } | |
| var body: some View { | |
| PickerMenuRow { | |
| let menus = makeMenus() | |
| ForEach(menus.indices, id: \.self) { offset in | |
| menus[offset] | |
| } | |
| } | |
| .fixedSize() | |
| } | |
| private func makeMenus() -> [AnyView] { | |
| var result: [AnyView] = [] | |
| for (selection, options) in repeat (each selections, each options) { | |
| result.append(AnyView(PickerMenu(selection: selection, options: options))) | |
| } | |
| return result | |
| } | |
| } | |
| extension PackedPickerMenuRow where repeat each Item: CaseIterable { | |
| @_disfavoredOverload init(_ selections: repeat Binding<each Item>) { | |
| self.init(repeat each selections, options: repeat Array((each Item).allCases)) | |
| } | |
| } | |
| // MARK: - PickerMenuRow | |
| /// 複数の `View`(主に ``PickerMenu``)を横一列に並べ、各要素の間にセパレーターを挿入するコンテナビュー | |
| struct PickerMenuRow<Content: View>: View { | |
| /// 子ビュー群を生成する `ViewBuilder` | |
| private let content: () -> Content | |
| /// `content` が返すビュー群を横並びで配置する `PickerMenuRow` を生成 | |
| init(@ViewBuilder content: @escaping () -> Content) { | |
| self.content = content | |
| } | |
| /// OS バージョンに応じて実装を切り替えたレイアウト本体 | |
| var body: some View { | |
| if #available(iOS 18.0, *) { | |
| subviewsLayout | |
| } else { | |
| variadicLayout | |
| } | |
| } | |
| // MARK: iOS 18+ 実装(Group(subviews:)) | |
| /// iOS 18+ で使用するレイアウト実装 | |
| /// 末尾以外の要素の後ろに ``separator`` を挿入します。 | |
| @available(iOS 18.0, *) | |
| private var subviewsLayout: some View { | |
| HStack(spacing: 0) { | |
| Group(subviews: content()) { subviews in | |
| ForEach(subviews: subviews) { subview in | |
| subview | |
| if subviews.last?.id != subview.id { separator } | |
| } | |
| } | |
| } | |
| .fixedSize() | |
| } | |
| // MARK: iOS 17 以下実装(_VariadicView) | |
| /// iOS 17 以下で使用するレイアウト実装 | |
| private var variadicLayout: some View { | |
| _VariadicView.Tree(PickerMenuRowRoot(separator: separator), content: content) | |
| .fixedSize() | |
| } | |
| // MARK: Separator | |
| /// 要素間に挿入される区切り線ビュー | |
| private var separator: some View { | |
| Rectangle() | |
| .frame(width: 1) | |
| .padding(.vertical, 8) | |
| .foregroundStyle(.gray) | |
| } | |
| } | |
| // MARK: - PickerMenuRowRoot | |
| /// `_VariadicView` による子ビュー列の列挙を行い、横並びとセパレーター挿入を実現するルート | |
| private struct PickerMenuRowRoot<Separator: View>: _VariadicView_MultiViewRoot { | |
| let separator: Separator | |
| func body(children: _VariadicView.Children) -> some View { | |
| HStack(spacing: 0) { | |
| ForEach(children) { child in | |
| child | |
| if children.last?.id != child.id { separator } | |
| } | |
| } | |
| } | |
| } | |
| // MARK: - PickerMenu | |
| /// `Menu` + `Picker` を用いた、コンパクトな単一選択メニュー | |
| struct PickerMenu<Item: Hashable & Identifiable>: View { | |
| /// 現在選択中の値。 | |
| @Binding var selection: Item | |
| /// 選択肢一覧。 | |
| var options: [Item] | |
| /// `Menu` 内に `Picker` を配置したメニュー UI を構築します。 | |
| var body: some View { | |
| Menu { | |
| Picker(String(describing: selection), selection: $selection) { | |
| ForEach(options) { item in | |
| Text(String(describing: item)).tag(item) | |
| } | |
| } | |
| } label: { | |
| Text(String(describing: selection)) | |
| .padding(.vertical, 8) | |
| .padding(.horizontal, 12) | |
| } | |
| .menuOrder(.fixed) | |
| .fixedSize() | |
| } | |
| } | |
| // MARK: - Foo | |
| /// プレビュー用のサンプル列挙型。 | |
| enum Foo: CaseIterable, Identifiable { | |
| case foo1 | |
| case foo2 | |
| case foo3 | |
| var id: Self { self } | |
| } | |
| // MARK: - Bar | |
| /// プレビュー用のサンプル列挙型。 | |
| enum Bar: CaseIterable, Identifiable { | |
| case bar1 | |
| case bar2 | |
| case bar3 | |
| var id: Self { self } | |
| } | |
| // MARK: - Baz | |
| /// プレビュー用のサンプル列挙型。 | |
| enum Baz: CaseIterable, Identifiable { | |
| case baz1 | |
| case baz2 | |
| case baz3 | |
| var id: Self { self } | |
| } | |
| // MARK: - Union | |
| /// 異なるドメイン列挙型(``Foo`` / ``Bar`` / ``Baz``)を 1 つの型にまとめる共用体 | |
| enum Union: Hashable, Identifiable, CustomStringConvertible { | |
| case foo(Foo) | |
| case bar(Bar) | |
| case baz(Baz) | |
| var id: Self { self } | |
| var description: String { | |
| switch self { | |
| case let .foo(foo): | |
| String(describing: foo) | |
| case let .bar(bar): | |
| String(describing: bar) | |
| case let .baz(baz): | |
| String(describing: baz) | |
| } | |
| } | |
| static func options(for value: Self) -> [Self] { | |
| switch value { | |
| case .foo: | |
| Foo.allCases.map(foo) | |
| case .bar: | |
| Bar.allCases.map(bar) | |
| case .baz: | |
| Baz.allCases.map(baz) | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment