1

I have an app and I want to implement an option to change the app's accent color but only with a certain selection of colors and I want them inside of a picker with a label with a circle of each color option's color

I've tried using a picker inside of a menu (because I want the label to not have the image) and applying .foregroundStyle, .tint and .background with no result and I'm starting to think it is not possible or not easy to do so. Here is my current code. Thanks.

Menu {
  Picker("", selection: vm.$selectedColor) {
    ForEach(colorOptions.allCases) { option in
      Label(option.rawValue.capitalized, systemImage: "circle.fill")
        .foregroundColor(option.color)
        .tag(option)
    }
  }
}  label: {
    Text("Select color")
}

ColorOptions:

enum colorOptions: String, CaseIterable, Identifiable {
  
  var id: String { self.rawValue }
  
  case indigo
  case red
  case green
  case blue
  case yellow
  case orange
  case purple
  
  var color: Color {
    switch self {
    case .indigo: return .indigo
    case .red: return .red
    case .green: return .green
    case .blue: return .blue
    case .yellow: return .yellow
    case .orange: return .orange
    case .purple: return .purple
    }
  }
}

Note that these are not the colors i want to use those were for testing the picker

3
  • You cannot change the color of the label title. However, you can at least show a color swab next to the title. See SwiftUI Picker issue with Rectangle() instead of Text() Commented Sep 26, 2024 at 15:07
  • 1
    @BenzyNeez It turns out there is a simple way to change the icon color inside a picker. See the new answer I added. Commented Mar 26 at 20:37
  • @BenzyNeez Strangely, after updating to iOS 18.4 all icons became the same color again on device and i had to use an additional .tint modifier to restore the individual colors (they still showed multiple colors in Xcode Previews). I updated my answer to reflect this. I don't know if this is a bug or not. Commented Apr 11 at 15:49

2 Answers 2

4

I am revisiting my previous answer with this new answer, since I figured out that there is a way to change the color of the icon inside a Picker.

I stumbled upon this when I was looking at the documentation for .foregroundStyle when using primary and secondary levels:

.foregroundStyle(.blue, .orange)

Using this format on the icon inside the picker will allow it to be colored. This is because when using this initializer, it will use the .palette rendering mode for symbol images.

This means that we can also specify the .palette rendering mode and use the regular .foregroundStyle modifier to achieve the same effect (UPDATE: Turns out .tint is also needed for iOS 18.4):

Picker("Select color", selection: $selectedColor) {
    ForEach(PickerColor.allCases) { option in
        Label {
            Text(option.rawValue.capitalized)
        } icon: {
            Image(systemName: "circle.fill")
                .symbolRenderingMode(.palette) // <- specify .palette rendering mode
                .foregroundStyle(option.color) // <- use .foregroundStyle as usual
                                .tint(option.color) // required for iOS 18.4
        }
        .tag(option)
    }
}

To have the picker's label also be colored using the selected color, use the .tint modifier on the picker:

.tint(selectedColor.color)

Here's the complete code to try:

import SwiftUI

enum PickerColor: String, CaseIterable, Identifiable {
    
    var id: String { self.rawValue }
    
    case indigo
    case red
    case green
    case blue
    case yellow
    case orange
    case purple
    
    var color: Color {
        switch self {
            case .indigo: return .indigo
            case .red: return .red
            case .green: return .green
            case .blue: return .blue
            case .yellow: return .yellow
            case .orange: return .orange
            case .purple: return .purple
        }
    }
}

struct PickerIconColor: View {
    
    //State values
    @State private var showMenu = false
    @State private var selectedColor: PickerColor = .indigo
    
    //Body
    var body: some View {
        
            Picker("Select color", selection: $selectedColor) {
                ForEach(PickerColor.allCases) { option in
                    Label {
                        Text(option.rawValue.capitalized)
                    } icon: {
                        Image(systemName: "circle.fill")
                            .symbolRenderingMode(.palette)
.foregroundStyle(option.color)
.tint(option.color)
                    }
                        .tag(option)
                }
            }
            .tint(selectedColor.color)
    }
}


#Preview("PickerMenuColorAlt") {
    PickerIconColor()
}

enter image description here

BONUS: For more control over the selected option styling, wrap the picker in a Menu with a custom label:

Menu {
    Picker("Select color", selection: $selectedColor) {
        ForEach(PickerColor.allCases) { option in
            Label {
                Text(option.rawValue.capitalized)
            } icon: {
                Image(systemName: "circle.fill")
                    .symbolRenderingMode(.palette)
                    .foregroundStyle(option.color)
                                        .tint(option.color)
            }
            .tag(option)
        }
    }
} label : {
    HStack {
        Label {
            Text(selectedColor.rawValue.capitalized)
        } icon: {
            Image(systemName: "circle.fill")
                .foregroundStyle(selectedColor.color)
                .imageScale(.small)
        }
        
        //Chevron icon to make it look like the picker
        Image(systemName: "chevron.up.chevron.down")
            .imageScale(.small)
    }
}
.tint(selectedColor.color)

enter image description here

For even more control, use an HStack instead of a Label to set spacing between the icons and text.

UPDATE iOS 18.4:

For some strange reason, after updating to iOS 18.4, all icons in all menus turned the same color blue on device, while still showing multi colored in XCode Previews. After some poking, I figured out they now require both a .foregroundStyle and a .tint (set to the same color) to restore the individual color of each icon. I don't know if this is intentional or a bug.

I updated the code above to reflect this.

Looks like the change could be related to this from the iOS 18.4 release notes:

  • Fixed: A color set by the tint(_:) modifier does not override the tint color of buttons in that view’s confirmation dialogs and alerts. (138774306)

Sign up to request clarification or add additional context in comments.

Comments

0

As far as I know, menus and pickers will ignore any styling like color, font, etc.

For maximum control, you will need to build your own menu. I started working on one a while back, but it's really just the core structure right now.

Here's the code:

import SwiftUI

enum pickerColor: String, CaseIterable, Identifiable {
    
    var id: String { self.rawValue }
    
    case indigo
    case red
    case green
    case blue
    case yellow
    case orange
    case purple
    
    var color: Color {
        switch self {
            case .indigo: return .indigo
            case .red: return .red
            case .green: return .green
            case .blue: return .blue
            case .yellow: return .yellow
            case .orange: return .orange
            case .purple: return .purple
        }
    }
}

struct PickerMenuColor: View {
    
    //State values
    @State private var showMenu = false
    @State private var selectedColor: pickerColor = .indigo

    //Body
    var body: some View {
        
        VStack {
            
            //Button that opens the menu
            Button {
                showMenu.toggle() //this can also be wrapped in withAnimation {showMenu.toggle()}
            } label: {
                //Button label
                HStack {
                    Text("\(selectedColor.rawValue.capitalized)")
                    
                    //Label chevron - comment out if picker style icon not needed
                    Image(systemName: "chevron.up.chevron.down")
                        .font(.system(size: 13))
                }
                //Label color
                .foregroundStyle(selectedColor.color)
            }
            .overlay(alignment: .bottom) {
                if showMenu {
                    VStack(spacing: 5) {
                        
                        //Make cases into an array for having access to an index
                        let pickerColorsArray = Array(pickerColor.allCases.enumerated())
                        
                        //Menu loop
                        ForEach(pickerColorsArray, id: \.element ) { index, color in
                            
                            //Option row
                            Button {
                                withAnimation {
                                    selectedColor = color
                                    showMenu.toggle() //comment this out if you want menu to remain open after making a selection
                                }
                                
                            } label : {
                                //Option label
                                VStack(alignment: .leading, spacing: 5) {
                                    HStack {
                                        Label {
                                            Text("\(color.rawValue.capitalized)")
                                        } icon: {
                                            Image(systemName: selectedColor == color ? "checkmark" : "circle")
                                                .font(.system(size: 12))
                                                .opacity(color == selectedColor ? 1 : 0)
                                        }
                                        
                                        //Horizontal spacing
                                        Spacer()
                                        
                                        //Colored circle icon
                                        Image(systemName: "circle.fill")
                                            .foregroundStyle(color.color)
                                    }
                                    
                                    //Row padding
                                    .padding(.horizontal)
                                    .padding(.vertical, 5)
                                    
                                    //Add divider after each row except for the last row
                                    if index != pickerColorsArray.count - 1 {
                                        Divider()
                                            .frame(height: 5)
                                    }
                                }
                            }
                            //Option row text color (button tint)
                            .tint(.black)
                        }
                    }
                    //Menu width
                    .frame(width: 200) //use .fixedSize() instead of .frame if custom width not needed
                    
                    //Menu top and bottom padding
                    .padding(.vertical, 10)
                    
                    //Menu background
                    .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 20))
                    
                    //Menu position (customize as needed, additional logic may be required here)
                    .offset(y: -30)
                    
                    //Menu shadow
                    .shadow(color: .black.opacity(0.2) , radius: 70)
                }
            }
        }
    }
}

#Preview {
    PickerMenuColor()
}

enter image description here

Note: When posting code for questions, make sure it's complete and reproducible so it can be copied and pasted without the need for further guesses and additions.

UPDATE: See the new answer added for a native way of coloring the icons inside the picker.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.