Color swatches in SwiftUI picker?

I'm building an application for macOS. Currently targeting 11 and up. I am running 11.3.1 (20E241) and Xcode 12.5 (12E262).

My application interacts with a remote server and downloads a lot of objects from it. These objects have a lot of properties, one of which is a color. The vendor has a list of about 35 named colors which are allowed, and those are the only values they accept. My application is meant to edit these objects, then push the changes to the server.

I'm currently trying to build a picker to let users select the color they want the object to be. I've built the colors in my Assets.xcassets, and I can use them in SwiftUI like so:

Code Block Swift
struct ObjectNamePlusIcon: View {
let object:genericObject
var body: some View {
HStack {
ObjectIcon(forObject: object).foregroundColor(Color(object.color ?? "black"))
Text(object.name)
}
.lineLimit(1)
.padding(2)
}
}

The icon shows up and is colored appropriately. I tried adding this to my object editor, though, and it isn't working:

Code Block Swift
Picker("Color:", selection: pickerColor) {
ForEach(namedColors, id: \.self) { colorName in
HStack {
Image(systemName: "square.fill")
.foregroundColor(Color(colorName))
Text(colorName)
}
}
}

namedColors is a list I have manually built of all of the allowed color names. With the above code, I get a menu, and the items have the square swatches, but all the swatches are black rather than the color they're meant to be. If I add .pickerStyle(RadioGroupPickerStyle()) to the end of the picker definition, I get a big radio group with swatches and names exactly how I want them.

I've tried this with the various renderingMode options on the image, tried turning the individual items into Buttons and wrapping them in a Menu, tried switching from HStack{Image,Text} to Label items, everything I can think of.

I know menus in general can have colored images in them. Safari has color favicons in the history list, for example. Am I missing something? Is this just something SwiftUI doesn't handle?

Accepted Reply

Figured it out. I used a modified version of this NSImage+TintColor extension to create a swatch dictionary:

let namedColors:[String] = [
	"aquamarine",
	"black",
	"blue",
	...
}

let myColorSwatches:[String:NSImage] = { (colorNames:[String]) -> [String:NSImage] in
	var toReturn:[String:NSImage] = [:]
	for colorName in colorNames {
		let image = NSImage(systemSymbolName: "rectangle.fill", accessibilityDescription: nil)!
		image.isTemplate = false
		image.lockFocus()
		NSColor(named: colorName)!.set()
		let imageRect = NSRect(origin: .zero, size: image.size)
		imageRect.fill(using: .sourceIn)
		image.unlockFocus()
		toReturn[colorName] = image
	}
	return toReturn
}(namedColors)

... which I can then reference in my SwiftUI Picker:

Picker("Color:", selection: pickerColor) {
	ForEach(namedColors, id: \.self) { colorName in
		HStack {
			Image(nsImage: myColorSwatches[colorName]!)
			Text(colorName)
		}
	}
}

And just as a reminder in case anybody else wants to do this, I have a color set defined in my Assets.xcassets for each named color.

Replies

Figured it out. I used a modified version of this NSImage+TintColor extension to create a swatch dictionary:

let namedColors:[String] = [
	"aquamarine",
	"black",
	"blue",
	...
}

let myColorSwatches:[String:NSImage] = { (colorNames:[String]) -> [String:NSImage] in
	var toReturn:[String:NSImage] = [:]
	for colorName in colorNames {
		let image = NSImage(systemSymbolName: "rectangle.fill", accessibilityDescription: nil)!
		image.isTemplate = false
		image.lockFocus()
		NSColor(named: colorName)!.set()
		let imageRect = NSRect(origin: .zero, size: image.size)
		imageRect.fill(using: .sourceIn)
		image.unlockFocus()
		toReturn[colorName] = image
	}
	return toReturn
}(namedColors)

... which I can then reference in my SwiftUI Picker:

Picker("Color:", selection: pickerColor) {
	ForEach(namedColors, id: \.self) { colorName in
		HStack {
			Image(nsImage: myColorSwatches[colorName]!)
			Text(colorName)
		}
	}
}

And just as a reminder in case anybody else wants to do this, I have a color set defined in my Assets.xcassets for each named color.

For Mac Catalyst I found this works well:

Picker(selection: $selectedColorOption, label: Text("Color")) {
    HStack {
        Image(uiImage: colorSwatchImage(with: .red))
        Text("Red")
    }.tag(1)
}
.pickerStyle(.menu)

private func colorSwatchImage(color: UIColor) -> UIImage {
    let size = CGSize(width: 24, height: 12)
    let rect = CGRect(origin: .zero, size: size)
    let renderer = UIGraphicsImageRenderer(bounds: rect)
    return renderer.image(actions: { context in
        color.setFill()
        UIBezierPath(roundedRect: rect, cornerRadius: 2).fill()
    })
}