Update: Huge thanks to Justin.
Retain the same code as the system image but use the asset inspector to change the SVG resource to a template image! So much easier and better. Thank you, Justin!
Coloring a SFIcon is simple. Here’s the default rendering:
struct ContentView: View { var body: some View { Image(systemName: "bandage") .resizable() .aspectRatio(contentMode: .fit) .padding() } }
And here’s the same using a red tint:
Image(systemName: "bandage") .resizable() .aspectRatio(contentMode: .fit) .foregroundColor(.red) .padding()
But what about the new SVG image support? (The seal image is by mungang kim, the Noun project):
SVGs carry their own color information. I edited the seal in Adobe Illustrator CS4 (I have a computer dedicated to Mojave) to add intrinsic colors:
Again, the foregroundColor(.red)
modifier is ignored and the native colors are shown. From my developer’s point of view, what I want is to be able to modify the SVG asset to use normal SwiftUI coloring. So the first thing I did was to create a ZStack
and use blending modes to set my foreground color.
I finally got my first hint of victory by using a content blend with .colorDodge
:
I discovered, though, that dodge wasn’t a great choice for non B&W assets. I needed a better blending mode.
When I tried to layer images in a ZStack
, I discovered the color mode would bleed through:
I needed to:
- Use a better blend mode that wouldn’t be affected by the SVG
Image
source colors and whether they were nativeColor
s (likeColor.red
or system ones (likeColor(.red)
, which usesUIColor
/NSColor
). - Isolate the blend mode so it wouldn’t affect other
View
s. - Move that functionality to a simple modifier, allowing a SVG
Image
to blend with a color.
I soon discovered that the sourceAtop
blend mode got me the coloring I needed, whether I used the B&W or colorized asset:
ZStack { content color.blendMode(.sourceAtop) }
Then, I needed to isolate the blend. I first turned to .drawingGroup(opaque:false)
but it kept failing to provide the result I was aiming for until I discovered that isolating that into its own VStack
bypassed any blends with ZStack
elements at the same level:
VStack { ZStack { content color.blendMode(.sourceAtop) } .drawingGroup(opaque: false) }
I then moved this into a custom View
modifier:
public struct ColorBlended: ViewModifier { fileprivate var color: Color public func body(content: Content) -> some View { VStack { ZStack { content color.blendMode(.sourceAtop) } .drawingGroup(opaque: false) } } } extension View { public func blending(color: Color) -> some View { modifier(ColorBlended(color: color)) } }
This allowed me to create a standard SwiftUI ZStack
that used the modifier in a normal cascade:
struct ContentView: View { var body: some View { ZStack { Image(systemName: "bandage.fill").resizable() .aspectRatio(contentMode: .fit) Image("ColorizedSeal") .resizable() .aspectRatio(contentMode: .fit) .padding(100) .blending(color: Color(.red)) } } }
Here’s how that renders:
You’ll want to make sure the blending happens after the image resizable
and aspectRatio
calls but other than that it can appear before or after the padding.
What I got out of this was a way to use Xcode 12’s new SVG asset support, standard SwiftUI layout, and flexibility when applying my color blend to assets that might not just be black and white.
I hope this helps others. If you have thoughts, corrections, or suggestions, let me know.