swiftui

How to make a SwiftUI Picker with arbitrary views as content


I'm curious, what is the best way of creating a Picker that contains arbitrary views as the entries? The documentation seems to suggest that you could use any View for the contents of the picker, but in practice, it ignores much of the content you provide.

I'm trying to make a menu that looks something like the color pickers in the Rendering Inspector of the SF Symbols app, looking like this (the Picker menu is at the bottom of the image):

color menu example

This code looked like it should do the trick:

struct PickerWithArbitraryView: View {
    @State private var selectedColor = "Black"
    var body: some View {
        Form {
            Picker("Color", selection: $selectedColor) {
//            VStack {
                HStack {
                    Rectangle()
                        .foregroundStyle(.black)
                        .frame(width: 10, height: 10)
                    Text("Black")
                    Spacer()
                }
                .tag("Black")
                
                HStack {
                    Rectangle()
                        .foregroundStyle(.red)
                        .frame(width: 10, height: 10)
                    Text("Red")
                    Spacer()
                }.tag("Red")
            }
        }
    }
}

But the result is this:

result of code

The Rectangle in my code is being ignored. If you replace the Picker line in the code with the commented out VStack below, you can see that the Views I've supplied as content are correct.

I'm using this initializer for Picker. The only requirement it mentions is that the content should conform to View. If SwiftUI is designed to extract just the first Text and first Image from the content, and this certainly seems to be the way it behaves, I'm surprised that it isn't mentioned in the docs anywhere. Am I missing something?

If there's a way to use arbitrary views as the contents, please let me know. Failing that, does someone have a way of specifically getting color samples into the picker?


Solution

  • I'm surprised that it isn't mentioned in the docs anywhere

    I assume that's because this makes it easier for Apple to change this behaviour in the future. What views the picker uses and how it lays them out is very OS-dependent, and can change in future updates.

    As of right now, the behaviour is exactly as you have guessed - SwiftUI extracts a "textual element" and a "image-like" element from each view you give in the view builder. Note that the placement of the image is on the leading edge on macOS, but on trailing edge on iOS.

    If you want your picker to look exactly a certain way, better not use the built-in Picker.

    That said, if you are happy with the colored rectangle appearing on the trailing edge, you can use the rectangle.fill symbol with rendering mode .palette.

    // e.g. a red rectangle
    Image(systemName: "rectangle.fill")
        .symbolRenderingMode(.palette)
        .foregroundStyle(.red)
    

    If you'd like a rectangle with a custom size, you can manually create an Image from the color.

    extension Color {
        func asImage(size: CGSize = .init(width: 25, height: 16), cornerRadius: CGFloat = 4) -> Image {
            Image(size: size) { context in
                context.fill(
                    Path(
                        roundedRect: .init(origin: .zero, size: size), 
                        cornerRadius: cornerRadius
                    ),
                    with: .color(self))
            }
        }
    }
    
    // Usage:
    HStack {
        Color.black.asImage()
        Text("Black")
    }
    .tag("Black")