swiftswiftuipicker

How to remove icon from Picker label?


I want to achieve these menus with a picker. The selected option only shows the text "Heart Rate" for example. And "<140 BPM" without the sf symbol. picker with only text options picker options showing an icon to the right picker options showing colored icons to the right

And currently I have 2 problems. When using a "Label" or an "HStack" the menu shows an sf symbol before the text, like shown in this image down below. And the 2nd problem is that the symbols are not taking the accent color, not even with .tint() or .foregroundStyle() or .foregroundColor()

picker options with icon picker options with white icons instead of colored ones

Here is the code I'm using

Picker(selection: $viewModel.heartRateAlert) {
    ForEach(HeartRateAlertOptions.allCases, id: \.self) { type in
        Label(type.title, systemImage: type.icon)
           .accentColor(type.iconColor) // this is not working, it's still white.
    }
} label: {
    Text("Heart Rate") // this should only show the text but its showing everything instead
}


Solution

  • Here are some ways to work around the limitations of the default Picker styling:

    Colored icons

    If you try to apply a tint or foreground style to a Label used as a menu item, the color gets ignored.

    So, instead of creating the Label using the name of a system symbol, you can supply a colored Icon. This can be created using an Image.

    I found that it doesn't work to create a colored Image by supplying a system name in the usual way. [EDIT: Actually it can be done, Sweeper's answer shows how. But you don't have any control over the size or weight of the icon when doing it that way.] So another way is to use the initializer that takes drawing instructions and draw the symbol using a GraphicsContext:

    private func coloredSymbol(type: HeartRateAlertOptions) -> some View {
        let symbol = Image(systemName: type.icon)
        let size = CGSize(width: 20, height: 20)
        return Image(size: size) { ctx in
            let resolvedSymbol = ctx.resolve(symbol)
            ctx.draw(
                resolvedSymbol,
                in: CGRect(origin: .zero, size: size)
            )
        }
        .foregroundStyle(type.iconColor)
    }
    

    Removing the icon from the label for the current selection

    As you have probably discovered, it does not seem to be possible to use different label styles for the menu items and for the current selection. If you try to set a .labelStyle, it gets used for the menu items and the current selection.

    As a workaround, you can use an overlay to mask the label for the current selection.

    EDIT: Sweeper's answer shows a better way to replace the label, I'll concede on that part!

    Putting it all together

    For testing, I created the following improvised version of the enum HeartRateAlertOptions. BTW, your screenshots suggest that you may have an issue with the titles of your enum values, two of the values seem to have the same title. Also, you may want to re-think the labelling for the first and last items (is it really <140, or <141?).

    enum HeartRateAlertOptions: CaseIterable {
        case below140
        case range140_152
        case range153_165
        case range166_177
        case over177
    
        var title: String {
            switch self {
            case .below140: "<140 BPM"
            case .range140_152: "140-152 BPM"
            case .range153_165: "153-165 BPM"
            case .range166_177: "166-177 BPM"
            case .over177: "+178 BPM"
            }
        }
    
        var icon: String {
            switch self {
            case .below140: "1.circle"
            case .range140_152: "2.circle"
            case .range153_165: "3.circle"
            case .range166_177: "4.circle"
            case .over177: "5.circle"
            }
        }
    
        var iconColor: Color {
            switch self {
            case .below140: .blue
            case .range140_152: .cyan
            case .range153_165: .green
            case .range166_177: .orange
            case .over177: .pink
            }
        }
    }
    

    Here is the updated example, which runs standalone:

    @State private var heartRateAlert: HeartRateAlertOptions = .below140
    @Environment(\.colorScheme) private var colorScheme: ColorScheme
    
    var body: some View {
        Form {
            Picker("Heart Rate", selection: $heartRateAlert) {
                ForEach(HeartRateAlertOptions.allCases, id: \.self) { type in
                    Label {
                        Text(type.title)
                    } icon: {
                        coloredSymbol(type: type)
                    }
                }
            }
            .overlay(alignment: .trailing) {
                HStack(spacing: 5) {
                    Text(heartRateAlert.title)
                    Image(systemName: "chevron.up.chevron.down")
                        .dynamicTypeSize(.xSmall)
                }
                .font(.callout)
                .padding(.vertical, 8)
                .padding(.leading, 30)
                .foregroundStyle(Color(.secondaryLabel))
                .background(Color(colorScheme == .dark ? .systemGray6 : .systemBackground))
                .allowsHitTesting(false)
            }
        }
        .padding(.top, 400)
    }
    

    Screenshot