Skip to content

Instantly share code, notes, and snippets.

@Koshimizu-Takehito
Created January 4, 2026 07:48
Show Gist options
  • Select an option

  • Save Koshimizu-Takehito/2d849c426b81f62df97dcde35a404aa5 to your computer and use it in GitHub Desktop.

Select an option

Save Koshimizu-Takehito/2d849c426b81f62df97dcde35a404aa5 to your computer and use it in GitHub Desktop.
PickerMenuRowForEach/PackedPickerMenuRow
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