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.
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()
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
}
Here are some ways to work around the limitations of the default Picker
styling:
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)
}
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.
You can style the overlay any way you like, but you will probably want to mimic the styling of the default label, including the icon chevron.up.chevron.down
.
To work effectively as a mask, the background needs to match the default background of a form field. I wasn't able to identify a standard system background color that matches in both light and dark modes. However, you can switch colors depending on whether light or dark mode is in operation, if necessary.
Apply .allowsHitTesting(false)
to the overlay, so that taps propagate through to the real label underneath.
EDIT: Sweeper's answer shows a better way to replace the label, I'll concede on that part!
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)
}