├── 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 |
--------------------------------------------------------------------------------