Creating an SKSpriteNode from the SF Symbols font, in a different color
Asked Answered
S

3

6

I'm trying to create an SKSpriteNode with an image from the SF Symbols font, and while I can do it, I can't seem to make it any color other than black.

Here's my code:

let image = UIImage.init(systemName: "gear")
let colored = image!.withTintColor(.red)
let texture = SKTexture.init(image: colored)
let sprite = SKSpriteNode.init(texture: texture, size: CGSize.init(width: 32, height: 32))

Unfortunately, the resultant sprite always comes out in black (and not red).

What am I doing wrong?

Sememe answered 23/1, 2020 at 20:15 Comment(7)
Just a guess (not having tried it), but maybe withTintColor(.red, renderingMode: .alwaysOriginal)?Ji
Or maybe .alwaysTemplate, the point being whatever mode it's picking for the context of making a texture, try the other...Ji
@Ji I tried .alwaysOriginal and it was the same, I’ll give the other a shot and see if that makes any difference.Sememe
…. is the gear black?Nationality
also, in swift .init is optionalNationality
@Ji Ok I tried both and neither make a differenceSememe
The extension in this answer helped me https://mcmap.net/q/188417/-changing-uiimage-colorAdelleadelpho
N
9

I think the problem is the UIImage is a vector graphic, and only UIImageViews properly handle them. Perhaps we can force it to a bitmap image to get it to work properly.

Here is some experimental code you can try:

let image = UIImage(systemName: "gear").withTintColor(.red)

let data = image.pngData()
let newImage = UIImage(data:data) 
let texture = SKTexture(image: newImage)
let sprite = SKSpriteNode(texture: texture,size: CGSize(width: 32, height: 32))
Nationality answered 24/1, 2020 at 1:43 Comment(2)
This produces a completely white 32x32 box. When I view my code in the debugger, the colored image is red (correct) but when putting it into the texture it goes black again.Sememe
Ok, I had time to research. A symbol image renders the tint color given because it is a vector graphic. Pretty sure I have seen that only UIImageView can handle vector graphics properly. Updating for something elseNationality
R
4

You have to set colorBlendFactor on the sprite.

func makeSprite(symbolName: String) -> SKSpriteNode {
    let image = Image(systemName: symbolName)

    // Thanks Oskar!
    // https://mcmap.net/q/1067996/-convert-swiftui-view-to-nsimage
    // See my adapted version of his code further down; I
    // think you won't need it if you're on iOS, meaning
    // using UIImage, but that's all black magic to me.
    let renderedByOskar = image.renderAsImage()!

    let texture = SKTexture(image: renderedByOskar)
    let sprite = SKSpriteNode(texture: texture)

    sprite.color = .green
    sprite.colorBlendFactor = 1 // <-- This

    return sprite
}

A green symbol, thanks to black magic:

A green symbol, made by black magic

Many thanks to Oskar for the rendering extensions that enable me to do this on macOS, meaning, without UIImage:

class NoInsetHostingView<V>: NSHostingView<V> where V: View {
    override var safeAreaInsets: NSEdgeInsets {
        return .init()
    }
}

extension Image {
    func renderAsImage() -> NSImage? {
        let view = NoInsetHostingView(rootView: self)
        view.setFrameSize(view.fittingSize)
        return view.bitmapImage()
    }
}

public extension NSView {
    func bitmapImage() -> NSImage? {
        guard let rep = bitmapImageRepForCachingDisplay(in: bounds) else {
            return nil
        }

        cacheDisplay(in: bounds, to: rep)

        guard let cgImage = rep.cgImage else {
            return nil
        }

        return NSImage(cgImage: cgImage, size: bounds.size)
    }
}
Redmond answered 15/11, 2021 at 15:56 Comment(0)
T
1

A different solution with UIKit and without SwiftUI:

extension SKTexture {
    convenience init?(systemName: String, pointSize: CGFloat) {
        let config = UIImage.SymbolConfiguration(pointSize: pointSize)
        guard let symbol = UIImage(systemName: systemName)?.applyingSymbolConfiguration(config) else { return nil }

        let rect = CGRect(origin: .zero, size: symbol.size)
        UIGraphicsBeginImageContextWithOptions(rect.size, false, 0)

        UIColor.white.setFill()
        UIRectFill(rect)

        let ctx = UIGraphicsGetCurrentContext()
        ctx?.setBlendMode(.destinationIn)
        ctx?.draw(symbol.cgImage!, in: rect)

        let result = UIGraphicsGetImageFromCurrentImageContext()!
        UIGraphicsEndImageContext()

        self.init(image: result)
    }
}

Using the new extension:

    let tex = SKTexture(systemName: "gearshape.fill", pointSize: 32)
    let settings = SKSpriteNode(texture: tex)
    settings.color = .accent
    settings.colorBlendFactor = 1
    addChild(settings)
Tuppence answered 18/5 at 13:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.