Coloring SVG assets in SwiftUI

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 native Color s (like Color.red or system ones (like Color(.red), which uses UIColor/NSColor).
  • Isolate the blend mode so it wouldn’t affect other Views.
  • 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’s Image 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.