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.
6 Comments
It seems to create a lot of padding left and right.
I hand coded 100 points to make it really squeezed in
Thanks for your quick reply!
I’m talking about the custom view modifier itself, not the example.
There shouldn’t be any padding, or am I missing something?
See for a comparison:
It should look like: https://imgur.com/a/JrnUPqX
It actually looks like: https://imgur.com/a/cJCYp02
I have not used the new SVG support in Xcode 12, but I have used PDF vector support in previous versions of Xcode.
To make an image “tintable”, I have always marked the image as a template. You can do this in an Asset Catalog using the “Render As” dropdown (select “Template Image”). Or, with a
UIImage
, you can use the.withRenderingMode(_:)
method, specifying.alwaysTemplate
. Finally, SwiftUI’sImage
has a similar method,renderingMode(_:)
that accepts.template
.Once the image is a template image, you can color it using SwiftUI just you do an SF Symbol (similarly with UIKit).
Hope this works with SVGs just as well as it does with vector PDFs!
That seems to have worked! So much easier!
To be super clear (and for others), this should reduce your initial seal example to:
struct ContentView: View {
var body: some View {
Image("Seal")
.renderingMode(.template)
.resizable()
.aspectRatio(contentMode: .fit)
.foregroundColor(.red)
.padding()
}
}
which should leave you with a red seal, without affecting any other images, which I believe is what you wanted.
Of course, since the seal SVG is coming from an Asset Catalog, you should also be able to set it as a template there, instead of in code. However, for maximum flexibility, you could leave it as “Original” in the asset catalog, giving you the option to use the original coloring or recolor it in code.