BetterPicker

A better SwiftUI Picker. Use _Picker instead of Picker. Create styles with _PickerStyle.

Example

import SwiftUI
import BetterPicker

struct ContentView: View {
  enum DayOfWeek: String, CaseIterable {
    case monday, tuesday, wednesday, thursday, friday, saturday, sunday
    var title: String { rawValue.localizedCapitalized }
  }

  @State
  private var selection = DayOfWeek.tuesday

  @State
  private var isEnabled = true

  var body: some View {
    NavigationView {
      VStack(spacing: 15) {
        Spacer()
        _Picker("Testing", selection: $selection) {
          ForEach(DayOfWeek.allCases, id: \.self) {
            Text($0.title).tag($0)
          }
          Text("Not a real option.")
        }
        .pickerStyle(.arrow)
        .disabled(!isEnabled)
        .font(.largeTitle)
        Spacer()
        Button("Toggle Disabled") {
          isEnabled.toggle()
        }
        .buttonStyle(.bordered)
        .padding()
      }
      .navigationTitle(selection.title)
    }
    .navigationViewStyle(.stack)
  }
}

extension _PickerStyle where Self == ArrowStyle {
  static var arrow: ArrowStyle { .init() }
}

struct ArrowStyle: _PickerStyle {
  func makeBody(configuration: Configuration) -> some View {
    Style(configuration: configuration)
  }

  struct Style: View {
    let configuration: Configuration

    @Environment(\.isEnabled)
    private var isEnabled

    var body: some View {
      HStack(alignment: .selection) {
        Image(systemName: "arrow.right")
          .symbolVariant(.circle.fill)
          .symbolRenderingMode(.hierarchical)
          .selectionGuide()
          .foregroundColor(.accentColor)
        VStack(alignment: .leading) {
          configuration.content { view, tag in
              .init(makeOption(view, tag))
          }
        }
      }
      .foregroundColor(isEnabled ? .primary : .accentColor)
    }

    @ViewBuilder
    func makeOption(
      _ option: Configuration.Option,
      _ tag: Tag
    ) -> some View {
      switch tag {
      case let .tagged(tag):
        option
          .selectionGuide(tag == configuration.selection ? .selection : .center)
          .onTapGesture {
            withAnimation {
              configuration.selection = tag
            }
          }
      case .untagged:
        option
      }
    }
  }
}

extension VerticalAlignment {
  private enum SelectionAlignment : AlignmentID {
    static func defaultValue(in d: ViewDimensions) -> CGFloat {
      return d[.bottom]
    }
  }
  static let selection = VerticalAlignment(SelectionAlignment.self)
}

extension View {
  func selectionGuide(_ guide: VerticalAlignment = .selection) -> some View {
    alignmentGuide(
      guide,
      computeValue: { d in d[VerticalAlignment.center] }
    )
  }
}

GitHub

View Github