SwiftUI Picker — Why Menu Has the Better Label

Originally published on Medium: Setup SwiftUI Picker with a label (July 2024) and Create Generics Enum PickerView (January 2025).

The SwiftUI Picker with .inline style has a peculiar problem: you give it a label, and it shows a chevron instead.


The issue is well known and frustrating. PickerStyle.inline completely ignores the label and displays a lonely arrow without any context instead of the description. Apple’s own documentation says nothing about it.

Picker(selection: $gender) {
    Text(Gender.male.rawValue).tag(Gender.male)
    Text(Gender.female.rawValue).tag(Gender.female)
    Text(Gender.nonbinary.rawValue).tag(Gender.nonbinary)
} label: {
    Text("Select your Gender")
}

Picker with inline style showing only a chevron instead of the label

The label is declared but never displayed. The user only sees the chevron and has to guess what it stands for.


The Solution: Menu as a Wrapper

If you want a menu, you should actually use a menu. The Menu view takes the Picker as its content, and the label can be freely styled.

Menu {
    Picker("", selection: $gender) {
        ForEach(Gender.allCases) { data in
            Text(data.rawValue).tag(data)
        }
    }
    .pickerStyle(.inline)
} label: {
    HStack {
        Text(gender.rawValue)
            .font(.title3)
            .frame(maxWidth: 200)
        Image(systemName: "chevron.up.chevron.down")
            .padding(.trailing)
    }
    .frame(maxWidth: .infinity)
    .frame(height: 55)
    .background(.white)
    .cornerRadius(10)
    .accentColor(.pink)
}
AspectInline PickerMenu Wrapper
Label visibleNo, only a chevronYes, and fully customizable
Tappable areaOnly the chevron itselfThe entire button area
StylingHeavily restrictedFull SwiftUI modifiers

Picker label and the entire button area are tappable

The entire button area is now tappable, the label shows the current value, and the menu opens cleanly as an inline sheet.

The inline menu opens after tapping the styled area


Bonus: Generic Enum Picker

With three pickers in the same form, the code quickly becomes redundant. The solution is a generic PickerView that works with any conforming enum.

The Protocol

protocol PickerEnum: Hashable, CaseIterable {
    var stringValue: String { get }
}

The View

struct GenericPickerView<S: PickerEnum, Label: View, Content: View>: View {
    let selection: Binding<S>
    let label: Label
    @ViewBuilder var content: (S) -> Content

    var body: some View {
        Picker(selection: selection, label: label) {
            ForEach(S.allCases, id: \.self) {
                content($0)
            }
        }
    }
}

Usage

List {
    GenericPickerView(
        selection: $direction,
        label: Text("Direction"),
        content: { Text("\($0.stringValue.capitalized)").tag($0) }
    )
    GenericPickerView(
        selection: $orientation,
        label: Text("Orientation"),
        content: { Text("\($0.stringValue.capitalized)").tag($0) }
    )
}

Generic Picker with Direction and Orientation enums

Selected values in the generic Picker

Two pickers, zero redundancy. Need a new enum? Just conform to PickerEnum and you are done.

The RandomAccessCollection Pitfall

CaseIterable.AllCases does not automatically conform to RandomAccessCollection, which causes a compiler error when used in ForEach. There are three clean ways to solve this:

VariantAssessment
ForEach(Array(S.allCases), id: \.self)Works, but creates an array cast on every render
where S.AllCases == Array<S> on the structExplicit and safe
where AllCases: RandomAccessCollection on the protocolDefined once, valid everywhere

The third variant is the cleanest because the constraint is defined directly on the protocol and does not need to be repeated on every view.


Two patterns that belong in every SwiftUI project: Menu instead of inline Picker, and Generics instead of copy-paste.


Translated with the help of Claude