├── BetterPickerExamples.swiftpm ├── Assets.xcassets │ ├── Contents.json │ ├── AccentColor.colorset │ │ └── Contents.json │ └── AppIcon.appiconset │ │ └── Contents.json ├── MyApp.swift ├── .swiftpm │ └── xcode │ │ └── package.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── ContentView.swift ├── Package.swift ├── GridPickerView.swift ├── ArrowPickerView.swift └── ListPickerView.swift ├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Sources └── swiftui-betterpicker │ ├── _PickerStyle.swift │ ├── _DefaultPickerStyle.swift │ ├── _AnyPickerStyle.swift │ ├── _PickerValueTagKey.swift │ ├── _SelectedPickerValueKey.swift │ ├── _PickerStyleKey.swift │ ├── _VariadicView++.swift │ ├── _PickerStyleConfiguration.swift │ └── _Picker.swift ├── Package.swift └── README.md /BetterPickerExamples.swiftpm/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /BetterPickerExamples.swiftpm/MyApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @main 4 | struct MyApp: App { 5 | var body: some Scene { 6 | WindowGroup { 7 | ContentView() 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /BetterPickerExamples.swiftpm/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /BetterPickerExamples.swiftpm/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /BetterPickerExamples.swiftpm/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /BetterPickerExamples.swiftpm/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sources/swiftui-betterpicker/_PickerStyle.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public protocol _PickerStyle { 4 | 5 | associatedtype SelectionValue: Hashable 6 | associatedtype Body: View 7 | 8 | func makeBody(configuration: Configuration) -> Self.Body 9 | 10 | typealias Configuration = _PickerStyleConfiguration 11 | typealias _SelectionValue = AnyHashable 12 | } 13 | -------------------------------------------------------------------------------- /Sources/swiftui-betterpicker/_DefaultPickerStyle.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension _PickerStyle where Self == _DefaultPickerStyle { 4 | public static var automatic: Self { .init() } 5 | } 6 | 7 | public struct _DefaultPickerStyle: _PickerStyle { 8 | public func makeBody(configuration: Configuration) -> some View { 9 | Picker(selection: configuration.$selection) { 10 | configuration.options 11 | } label: { 12 | configuration.label 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/swiftui-betterpicker/_AnyPickerStyle.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | internal struct _AnyPickerStyle: _PickerStyle { 4 | private var _makeBody: (Configuration) -> AnyView 5 | 6 | init(_ style: S) where S.SelectionValue == SelectionValue { 7 | self._makeBody = { AnyView(style.makeBody(configuration: $0)) } 8 | } 9 | 10 | func makeBody(configuration: Configuration) -> some View { 11 | _makeBody(configuration) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /BetterPickerExamples.swiftpm/ContentView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct ContentView: View { 4 | var body: some View { 5 | NavigationView { 6 | List { 7 | NavigationLink("Arrow") { 8 | ArrowPickerView() 9 | } 10 | NavigationLink("List") { 11 | ListPickerView() 12 | } 13 | NavigationLink("Grid") { 14 | GridPickerView() 15 | } 16 | } 17 | .navigationTitle("BetterPicker") 18 | } 19 | .navigationViewStyle(.stack) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/swiftui-betterpicker/_PickerValueTagKey.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | private struct _PickerValueTagKey: EnvironmentKey { 4 | fileprivate static var defaultValue: _Tag = .untagged 5 | } 6 | 7 | extension EnvironmentValues { 8 | internal var _pickerValueTag: _Tag { 9 | get { self[_PickerValueTagKey.self] } 10 | set { self[_PickerValueTagKey.self] = newValue } 11 | } 12 | } 13 | 14 | extension EnvironmentValues { 15 | public var pickerValueTag: _Tag { 16 | get { self[_PickerValueTagKey.self] } 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /Sources/swiftui-betterpicker/_SelectedPickerValueKey.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | private struct _IsSelectedPickerValueKey: EnvironmentKey { 4 | fileprivate static var defaultValue: Bool = false 5 | } 6 | 7 | extension EnvironmentValues { 8 | internal var _isSelectedPickerValue: Bool { 9 | get { self[_IsSelectedPickerValueKey.self] } 10 | set { self[_IsSelectedPickerValueKey.self] = newValue } 11 | } 12 | } 13 | 14 | extension EnvironmentValues { 15 | public var isSelectedPickerValue: Bool { 16 | get { self[_IsSelectedPickerValueKey.self] } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.5 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "swiftui-betterpicker", 8 | platforms: [.iOS(.v13), .macOS(.v10_15), .tvOS(.v13), .watchOS(.v6)], 9 | products: [ 10 | .library( 11 | name: "BetterPicker", 12 | targets: ["swiftui-betterpicker"]), 13 | ], 14 | targets: [ 15 | .target( 16 | name: "swiftui-betterpicker", 17 | dependencies: []), 18 | ] 19 | ) 20 | -------------------------------------------------------------------------------- /Sources/swiftui-betterpicker/_PickerStyleKey.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension View { 4 | public func pickerStyle(_ style: S) -> some View where S.SelectionValue == AnyHashable { 5 | // FIXME: opening exestenials plz 6 | environment(\._pickerStyle, _AnyPickerStyle(style)) 7 | } 8 | } 9 | 10 | private struct _PickerStyleKey: EnvironmentKey { 11 | fileprivate static var defaultValue: _AnyPickerStyle { _AnyPickerStyle(.automatic) } 12 | } 13 | 14 | extension EnvironmentValues { 15 | internal var _pickerStyle: _AnyPickerStyle { 16 | get { self[_PickerStyleKey.self] } 17 | set { self[_PickerStyleKey.self] = newValue } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/swiftui-betterpicker/_VariadicView++.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | internal struct TraitCollection { 4 | static let traitsKey = "traits" 5 | static let traitsStorageKey = "storage" 6 | static let tagKey = "tagged" 7 | 8 | let traits: [Any] 9 | func tags() -> [Selection] { 10 | traits.compactMap { 11 | withUnsafePointer(to: Mirror(reflecting: $0).descendant(0, Self.tagKey)) { 12 | $0.pointee as? Selection 13 | } 14 | } 15 | } 16 | } 17 | 18 | extension _VariadicView.Children.Element { 19 | internal var _traits: TraitCollection { 20 | TraitCollection( 21 | traits: Mirror(reflecting: self) 22 | .descendant(TraitCollection.traitsKey, TraitCollection.traitsStorageKey) as? [Any] ?? [] 23 | ) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/swiftui-betterpicker/_PickerStyleConfiguration.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public struct _PickerStyleConfiguration { 4 | public struct Content: View { 5 | private let _view: AnyView 6 | internal init(_ view: V) { 7 | self._view = AnyView(view) 8 | } 9 | public var body: some View { _view } 10 | } 11 | 12 | public struct Option: View { 13 | private let _view: AnyView 14 | public init(_ view: V, tag: _Tag) { 15 | self._view = AnyView(view) 16 | self.tag = tag 17 | } 18 | public var tag: _Tag 19 | public var body: some View { _view } 20 | } 21 | 22 | public struct Label: View { 23 | private let _view: AnyView 24 | internal init(_ view: V) { 25 | self._view = AnyView(view) 26 | } 27 | public var body: some View { _view } 28 | } 29 | 30 | @Binding 31 | public var selection: SelectionValue 32 | public let options: Content 33 | public let content: (@escaping (Option) -> AnyView) -> Content 34 | public let label: Label 35 | } 36 | -------------------------------------------------------------------------------- /BetterPickerExamples.swiftpm/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.5 2 | 3 | // WARNING: 4 | // This file is automatically generated. 5 | // Do not edit it by hand because the contents will be replaced. 6 | 7 | import PackageDescription 8 | import AppleProductTypes 9 | 10 | let package = Package( 11 | name: "BetterPickerExamples", 12 | platforms: [ 13 | .iOS("15.2") 14 | ], 15 | products: [ 16 | .iOSApplication( 17 | name: "BetterPickerExamples", 18 | targets: ["AppModule"], 19 | bundleIdentifier: "com.BetterPickerExamples", 20 | teamIdentifier: "F9PGNEMEHU", 21 | displayVersion: "1.0", 22 | bundleVersion: "1", 23 | supportedDeviceFamilies: [ 24 | .pad, 25 | .phone 26 | ], 27 | supportedInterfaceOrientations: [ 28 | .portrait, 29 | .landscapeRight, 30 | .landscapeLeft, 31 | .portraitUpsideDown(.when(deviceFamilies: [.pad])) 32 | ] 33 | ) 34 | ], 35 | dependencies: [ 36 | .package(path: "/Users/ericlewis/Developer/swiftui-betterpicker") 37 | ], 38 | targets: [ 39 | .executableTarget( 40 | name: "AppModule", 41 | dependencies: [ 42 | .product(name: "BetterPicker", package: "swiftui-betterpicker") 43 | ], 44 | path: "." 45 | ) 46 | ] 47 | ) -------------------------------------------------------------------------------- /BetterPickerExamples.swiftpm/GridPickerView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import swiftui_betterpicker 3 | 4 | struct GridPickerView: View { 5 | enum Colors: CaseIterable { 6 | case red 7 | case blue 8 | case green 9 | case purple 10 | case yellow 11 | case pink 12 | 13 | var color: Color { 14 | switch self { 15 | case .red: return .red 16 | case .purple: return .purple 17 | case .green: return .green 18 | case .yellow: return .yellow 19 | case .blue: return .blue 20 | case .pink: return .pink 21 | } 22 | } 23 | } 24 | 25 | @State 26 | private var selection = Colors.red 27 | 28 | var body: some View { 29 | _Picker("Grid", selection: $selection) { 30 | ForEach(Colors.allCases, id: \.self) { 31 | $0.color.tag($0) 32 | } 33 | } 34 | .pickerStyle(.grid) 35 | .navigationTitle("Grid Picker") 36 | } 37 | } 38 | 39 | extension _PickerStyle where Self == GridPickerStyle { 40 | static var grid: GridPickerStyle { .init() } 41 | } 42 | 43 | struct GridPickerStyle: _PickerStyle { 44 | func makeBody(configuration: Configuration) -> some View { 45 | Style(configuration: configuration) 46 | } 47 | 48 | struct Style: View { 49 | let configuration: Configuration 50 | 51 | var body: some View { 52 | LazyVGrid(columns: [.init(), .init(), .init()]) { 53 | configuration.content { 54 | .init(makeOption($0)) 55 | } 56 | } 57 | } 58 | 59 | @ViewBuilder 60 | func makeOption( 61 | _ option: Configuration.Option 62 | ) -> some View { 63 | switch option.tag { 64 | case let .tagged(tag): 65 | Button { 66 | withAnimation { 67 | configuration.selection = tag 68 | } 69 | } label: { 70 | option 71 | .aspectRatio(1, contentMode: .fit) 72 | .overlay { 73 | if configuration.selection == tag { 74 | Image(systemName: "checkmark.circle.fill") 75 | .imageScale(.large) 76 | .foregroundColor(.primary) 77 | .blendMode(.luminosity) 78 | } 79 | } 80 | } 81 | .buttonStyle(.plain) 82 | case .untagged: 83 | EmptyView() 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /BetterPickerExamples.swiftpm/ArrowPickerView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import swiftui_betterpicker 3 | 4 | struct ArrowPickerView: View { 5 | enum DayOfWeek: String, CaseIterable { 6 | case monday, tuesday, wednesday, thursday, friday, saturday, sunday 7 | var title: String { rawValue.localizedCapitalized } 8 | } 9 | 10 | @State 11 | private var selection = DayOfWeek.tuesday 12 | 13 | @State 14 | private var isEnabled = true 15 | 16 | var body: some View { 17 | VStack(spacing: 15) { 18 | Spacer() 19 | _Picker("Testing", selection: $selection) { 20 | ForEach(DayOfWeek.allCases, id: \.self) { 21 | Text($0.title).tag($0) 22 | } 23 | Text("Not a real option.") 24 | } 25 | .pickerStyle(.arrow) 26 | .disabled(!isEnabled) 27 | .font(.largeTitle) 28 | Spacer() 29 | Button("Toggle Disabled") { 30 | isEnabled.toggle() 31 | } 32 | .buttonStyle(.bordered) 33 | .padding() 34 | } 35 | .navigationTitle(selection.title) 36 | } 37 | } 38 | 39 | extension _PickerStyle where Self == ArrowStyle { 40 | static var arrow: ArrowStyle { .init() } 41 | } 42 | 43 | struct ArrowStyle: _PickerStyle { 44 | func makeBody(configuration: Configuration) -> some View { 45 | Style(configuration: configuration) 46 | } 47 | 48 | struct Style: View { 49 | let configuration: Configuration 50 | 51 | @Environment(\.isEnabled) 52 | private var isEnabled 53 | 54 | var body: some View { 55 | HStack(alignment: .selection) { 56 | Image(systemName: "arrow.right") 57 | .symbolVariant(.circle.fill) 58 | .symbolRenderingMode(.hierarchical) 59 | .selectionGuide() 60 | .foregroundColor(.accentColor) 61 | VStack(alignment: .leading) { 62 | configuration.content { view in 63 | .init(makeOption(view)) 64 | } 65 | } 66 | } 67 | .foregroundColor(isEnabled ? .primary : .accentColor) 68 | } 69 | 70 | @ViewBuilder 71 | func makeOption( 72 | _ option: Configuration.Option 73 | ) -> some View { 74 | switch option.tag { 75 | case let .tagged(tag): 76 | option 77 | .selectionGuide(tag == configuration.selection ? .selection : .center) 78 | .onTapGesture { 79 | withAnimation { 80 | configuration.selection = tag 81 | } 82 | } 83 | case .untagged: 84 | option 85 | } 86 | } 87 | } 88 | } 89 | 90 | extension VerticalAlignment { 91 | private enum SelectionAlignment : AlignmentID { 92 | static func defaultValue(in d: ViewDimensions) -> CGFloat { 93 | return d[.bottom] 94 | } 95 | } 96 | static let selection = VerticalAlignment(SelectionAlignment.self) 97 | } 98 | 99 | extension View { 100 | func selectionGuide(_ guide: VerticalAlignment = .selection) -> some View { 101 | alignmentGuide( 102 | guide, 103 | computeValue: { d in d[VerticalAlignment.center] } 104 | ) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Sources/swiftui-betterpicker/_Picker.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public enum _Tag { 4 | case tagged(AnyHashable) 5 | case untagged 6 | 7 | var value: AnyHashable? { 8 | if case let .tagged(value) = self { 9 | return value 10 | } 11 | return nil 12 | } 13 | } 14 | 15 | public struct _Picker: View { 16 | private typealias Configuration = _PickerStyleConfiguration 17 | 18 | @Binding 19 | private var selection: AnyHashable 20 | 21 | @Environment(\._pickerStyle) 22 | private var style 23 | 24 | private let content: Content 25 | private let label: Label 26 | 27 | public init( 28 | selection: Binding, 29 | @ViewBuilder content: () -> Content, 30 | @ViewBuilder label: () -> Label 31 | ) { 32 | self._selection = .init(selection) 33 | self.content = content() 34 | self.label = label() 35 | } 36 | 37 | public var body: some View { 38 | style.makeBody( 39 | configuration: Configuration( 40 | selection: $selection, 41 | options: .init(content), 42 | content: { option in 43 | Configuration.Content( 44 | _VariadicView.Tree( 45 | Root( 46 | selection: selection, 47 | option: option 48 | ), 49 | content: { content } 50 | ) 51 | ) 52 | }, 53 | label: Configuration.Label(label) 54 | ) 55 | ) 56 | } 57 | 58 | private struct Root: _VariadicView.MultiViewRoot { 59 | typealias Children = _VariadicView.Children 60 | typealias Child = Children.Element 61 | 62 | let selection: AnyHashable 63 | let option: (Configuration.Option) -> AnyView 64 | 65 | func body(children: Children) -> some View { 66 | ForEach(makeTaggedViews(children), id: \.view.id) { tagged in 67 | option( 68 | .init( 69 | tagged.view 70 | .environment(\._isSelectedPickerValue, tagged.tag.value == selection) 71 | .environment(\._pickerValueTag, tagged.tag), 72 | tag: tagged.tag 73 | ) 74 | ) 75 | } 76 | } 77 | 78 | func makeTaggedViews(_ views: Children) -> [(view: Child, tag: _Tag)] { 79 | views.compactMap { view in 80 | guard let tag: SelectionValue = view._traits.tags().first else { 81 | return (view, .untagged) 82 | } 83 | return (view, .tagged(tag)) 84 | } 85 | } 86 | } 87 | } 88 | 89 | extension _Picker where Label == Text { 90 | public init( 91 | _ titleKey: LocalizedStringKey, selection: Binding, 92 | @ViewBuilder content: @escaping () -> Content 93 | ) { 94 | self.init(selection: selection, content: content) { 95 | Text(titleKey) 96 | } 97 | } 98 | } 99 | 100 | extension _Picker where Label == Text { 101 | @_disfavoredOverload 102 | public init( 103 | _ title: S, selection: Binding, @ViewBuilder content: @escaping () -> Content 104 | ) where S: StringProtocol { 105 | self.init(selection: selection, content: content) { 106 | Text(title) 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /BetterPickerExamples.swiftpm/ListPickerView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import swiftui_betterpicker 3 | 4 | struct ListPickerView: View { 5 | enum DayOfWeek: String, CaseIterable { 6 | case monday, tuesday, wednesday, thursday, friday, saturday, sunday 7 | var title: String { rawValue.localizedCapitalized } 8 | } 9 | 10 | @State 11 | private var selection = DayOfWeek.tuesday 12 | 13 | var body: some View { 14 | List { 15 | let title: LocalizedStringKey = "List Picker" 16 | _Picker(title, selection: $selection) { 17 | ForEach(DayOfWeek.allCases, id: \.self) { 18 | Text($0.title).tag($0) 19 | } 20 | } 21 | .pickerStyle(.list(title: title)) 22 | } 23 | .navigationTitle(selection.title) 24 | } 25 | } 26 | 27 | extension _PickerStyle where Self == ListPickerStyle { 28 | static func list(title: LocalizedStringKey) -> ListPickerStyle { .init(title: title) } 29 | } 30 | 31 | struct ListPickerStyle: _PickerStyle { 32 | let title: LocalizedStringKey 33 | 34 | func makeBody(configuration: Configuration) -> some View { 35 | Style(configuration: configuration, title: title) 36 | } 37 | 38 | struct Style: View { 39 | let configuration: Configuration 40 | let title: LocalizedStringKey 41 | 42 | var body: some View { 43 | NavigationLink { 44 | ListView(configuration: configuration, title: title) 45 | } label: { 46 | HStack { 47 | configuration.label 48 | Spacer() 49 | configuration.content { view in 50 | .init(makeBadge(view)) 51 | } 52 | } 53 | } 54 | } 55 | 56 | @ViewBuilder 57 | func makeBadge( 58 | _ option: Configuration.Option 59 | ) -> some View { 60 | switch option.tag { 61 | case let .tagged(tag): 62 | if tag == configuration.selection { 63 | option 64 | .font(.callout) 65 | .foregroundStyle(.secondary) 66 | } else { 67 | EmptyView() 68 | } 69 | case .untagged: 70 | EmptyView() 71 | } 72 | } 73 | } 74 | 75 | struct ListView: View { 76 | @Environment(\.dismiss) 77 | private var dismiss 78 | 79 | let configuration: Configuration 80 | let title: LocalizedStringKey 81 | 82 | var body: some View { 83 | List { 84 | configuration.content { view in 85 | .init(makeOption(view)) 86 | } 87 | } 88 | .navigationTitle(title) 89 | } 90 | 91 | @ViewBuilder 92 | func makeOption( 93 | _ option: Configuration.Option 94 | ) -> some View { 95 | switch option.tag { 96 | case let .tagged(tag): 97 | Button { 98 | configuration.selection = tag 99 | Task { 100 | try? await Task.sleep(nanoseconds: 100_000_000) 101 | DispatchQueue.main.async { 102 | dismiss() 103 | } 104 | } 105 | } label: { 106 | HStack { 107 | option 108 | .foregroundColor(.primary) 109 | Spacer() 110 | if tag == configuration.selection { 111 | Image(systemName: "checkmark") 112 | } 113 | } 114 | } 115 | case .untagged: 116 | EmptyView() 117 | } 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BetterPicker 2 | 3 | A better SwiftUI Picker. Use `_Picker` instead of `Picker`. Create styles with `_PickerStyle`. 4 | 5 | ### The is a WIP 6 | This library is currently a work-in-progress with regards to ironing out the API & documentation. 7 | Once both are in a good state a release will be cut. Until then you can point to `main` and deal with 8 | an unstable interface! Bug reports, testing, ideas, pull requests, and more are welcome! 9 | 10 | ## Examples 11 | You can find examples in the [`BetterPickerExamples`](BetterPickerExamples.swiftpm) App Playground. 12 | 13 | ```swift 14 | import SwiftUI 15 | import BetterPicker 16 | 17 | struct ContentView: View { 18 | enum DayOfWeek: String, CaseIterable { 19 | case monday, tuesday, wednesday, thursday, friday, saturday, sunday 20 | var title: String { rawValue.localizedCapitalized } 21 | } 22 | 23 | @State 24 | private var selection = DayOfWeek.tuesday 25 | 26 | @State 27 | private var isEnabled = true 28 | 29 | var body: some View { 30 | NavigationView { 31 | VStack(spacing: 15) { 32 | Spacer() 33 | _Picker("Testing", selection: $selection) { 34 | ForEach(DayOfWeek.allCases, id: \.self) { 35 | Text($0.title).tag($0) 36 | } 37 | Text("Not a real option.") 38 | } 39 | .pickerStyle(.arrow) 40 | .disabled(!isEnabled) 41 | .font(.largeTitle) 42 | Spacer() 43 | Button("Toggle Disabled") { 44 | isEnabled.toggle() 45 | } 46 | .buttonStyle(.bordered) 47 | .padding() 48 | } 49 | .navigationTitle(selection.title) 50 | } 51 | .navigationViewStyle(.stack) 52 | } 53 | } 54 | 55 | extension _PickerStyle where Self == ArrowStyle { 56 | static var arrow: ArrowStyle { .init() } 57 | } 58 | 59 | struct ArrowStyle: _PickerStyle { 60 | func makeBody(configuration: Configuration) -> some View { 61 | Style(configuration: configuration) 62 | } 63 | 64 | struct Style: View { 65 | let configuration: Configuration 66 | 67 | @Environment(\.isEnabled) 68 | private var isEnabled 69 | 70 | var body: some View { 71 | HStack(alignment: .selection) { 72 | Image(systemName: "arrow.right") 73 | .symbolVariant(.circle.fill) 74 | .symbolRenderingMode(.hierarchical) 75 | .selectionGuide() 76 | .foregroundColor(.accentColor) 77 | VStack(alignment: .leading) { 78 | configuration.content { view, tag in 79 | .init(makeOption(view, tag)) 80 | } 81 | } 82 | } 83 | .foregroundColor(isEnabled ? .primary : .accentColor) 84 | } 85 | 86 | @ViewBuilder 87 | func makeOption( 88 | _ option: Configuration.Option, 89 | _ tag: _Tag 90 | ) -> some View { 91 | switch tag { 92 | case let .tagged(tag): 93 | option 94 | .selectionGuide(tag == configuration.selection ? .selection : .center) 95 | .onTapGesture { 96 | withAnimation { 97 | configuration.selection = tag 98 | } 99 | } 100 | case .untagged: 101 | option 102 | } 103 | } 104 | } 105 | } 106 | 107 | extension VerticalAlignment { 108 | private enum SelectionAlignment : AlignmentID { 109 | static func defaultValue(in d: ViewDimensions) -> CGFloat { 110 | return d[.bottom] 111 | } 112 | } 113 | static let selection = VerticalAlignment(SelectionAlignment.self) 114 | } 115 | 116 | extension View { 117 | func selectionGuide(_ guide: VerticalAlignment = .selection) -> some View { 118 | alignmentGuide( 119 | guide, 120 | computeValue: { d in d[VerticalAlignment.center] } 121 | ) 122 | } 123 | } 124 | ``` 125 | --------------------------------------------------------------------------------