10

I have an image like this: enter image description here

(Rendered as a template image)

I tried this code:

@IBOutlet weak var imgAdd: NSImageView!
imgAdd.layer?.backgroundColor = CGColor.white

Which only changes the background color of course.

Is there a way to change the color of this image programmatically?


So far I've tried the code below which doesn't work. (The image color doesn't change.)

func tintedImage(_ image: NSImage, tint: NSColor) -> NSImage {
    guard let tinted = image.copy() as? NSImage else { return image }
    tinted.lockFocus()
    tint.set()

    let imageRect = NSRect(origin: NSZeroPoint, size: image.size)
    NSRectFillUsingOperation(imageRect, .sourceAtop)

    tinted.unlockFocus()
    return tinted
}

imgDok.image = tintedImage(NSImage(named: "myImage")!, tint: NSColor.red)
ocodo
  • 29,401
  • 18
  • 105
  • 117
Trombone0904
  • 4,132
  • 8
  • 51
  • 104

9 Answers9

16

Swift 4

Updated answer for Swift 4

Please note, this NSImage extension is based on @Ghost108 and @Taehyung_Cho's answers, so a larger credit goes to them.

extension NSImage {
    func tint(color: NSColor) -> NSImage {
        let image = self.copy() as! NSImage
        image.lockFocus()

        color.set()

        let imageRect = NSRect(origin: NSZeroPoint, size: image.size)
        imageRect.fill(using: .sourceAtop)

        image.unlockFocus()

        return image
    }
}
ocodo
  • 29,401
  • 18
  • 105
  • 117
  • 1
    Why must the image _not_ be a template? I figured it _should_ be? – Ky - Nov 14 '18 at 20:25
  • it works fine for me. I inverted the logic so that this _only_ applies the tint to template images, and all's good; now my template images behave as they should when custom-drawn: black on light backgrounds and white on dark ones. – Ky - Nov 15 '18 at 17:45
  • That's good to know. If that's the case, there need be no conditional logic at all, since both template and regular images can be tinted. – ocodo Nov 16 '18 at 00:33
  • All answers here using `image.lockFocus()` suffer from the same issues: they're not playing well with retina images on retina display (they draw at 1x) and don't play well with dynamic colors. Prefer the `NSImage` block-based drawing initializer instead, like [in this answer](https://stackoverflow.com/a/64049177/400056). – DarkDust Mar 14 '23 at 09:45
  • Note the swift version and date of the answers. Unless you have something useful to add, concentrate only on newer answers, or add your own if it's missing. No one should look to answers that were posted about Swift 4 in 2018. – ocodo Mar 14 '23 at 11:46
  • @DarkDust btw, you have the ability to make the changes I just did, also that hat. Chef's kiss. – ocodo Mar 14 '23 at 11:56
  • 1
    The [API I'm referring to is available since macOS 10.8](https://developer.apple.com/documentation/appkit/nsimage/1519860-init), that is 2012. Your answer isn't _wrong_ , but since it's the top answer I felt it's a good idea to point out issues with this approach that can be avoided. – DarkDust Mar 14 '23 at 13:38
11

Swift 5

This method should be used: (note the other answers, using NSImage.lockFocus() is deprecated macOS 10.0–13.0)

extension NSImage {
    func tint(color: NSColor) -> NSImage {
        return NSImage(size: size, flipped: false) { (rect) -> Bool in
            color.set()
            rect.fill()
            self.draw(in: rect, from: NSRect(origin: .zero, size: self.size), operation: .destinationIn, fraction: 1.0)
            return true
        }
    }
}

Note: When using .withAlphaComponent(0.5) on an NSColor instance, that color loses support for switching between light/dark mode. It's recommend to use color assets, to avoid the issue.

ocodo
  • 29,401
  • 18
  • 105
  • 117
Cyberbeni
  • 660
  • 4
  • 15
10

Swift 4 version

extension NSImage {
   func image(withTintColor tintColor: NSColor) -> NSImage {
       guard isTemplate else { return self }
       guard let copiedImage = self.copy() as? NSImage else { return self }
       copiedImage.lockFocus()
       tintColor.set()
       let imageBounds = NSMakeRect(0, 0, copiedImage.size.width, copiedImage.size.height)
       imageBounds.fill(using: .sourceAtop)
       copiedImage.unlockFocus()
       copiedImage.isTemplate = false
       return copiedImage
   }
}
Tiep Vu Van
  • 975
  • 8
  • 13
8

I found the solution with everyone's help:

(Swift 3)

func tintedImage(_ image: NSImage, tint: NSColor) -> NSImage {
    guard let tinted = image.copy() as? NSImage else { return image }
    tinted.lockFocus()
    tint.set()

    let imageRect = NSRect(origin: NSZeroPoint, size: image.size)
    NSRectFillUsingOperation(imageRect, .sourceAtop)

    tinted.unlockFocus()
    return tinted
}

imgDok.image = tintedImage(NSImage(named: "myImage")!, tint: NSColor.red)

Important: in interface builder I had to set the "render as" setting of the image to "Default".

ocodo
  • 29,401
  • 18
  • 105
  • 117
Trombone0904
  • 4,132
  • 8
  • 51
  • 104
  • please consider updating the answer to this question, so that the Swift 5 answer is marked as correct – ocodo Mar 14 '23 at 11:54
8

Since your image is inside an NSImageView, the following should work fine (available since macOS 10.14):

let image = NSImage(named: "myImage")!
image.isTemplate = true
let imageView = NSImageView(image: image)
imageView.contentTintColor = .green

The solution is to apply "contentTintColor" to your NSImageView instead of the NSImage.

See: Documentation

hkdalex
  • 717
  • 7
  • 13
6

Had to modify @Ghost108's answer little bit for Xcode 9.2.

NSRectFillUsingOperation(imageRect, .sourceAtop)

to

imageRect.fill(using: .sourceAtop)

Thanks.

Seokhwan Cho
  • 81
  • 1
  • 2
1

no need to copt: extension NSImage {

func tint(with color: NSColor) -> NSImage {
    self.lockFocus()
    color.set()
    let srcSpacePortionRect = NSRect(origin: CGPoint(), size: self.size)
    srcSpacePortionRect.fill(using: .sourceAtop)
    self.unlockFocus()
    return self
}

}

ingconti
  • 10,876
  • 3
  • 61
  • 48
0

Since you can't use the UIImage functions, you can try using CoreImage (CI). I don't know if there is an easier version but this one will work fore sure!

First you create the CIImage

    let image = CIImage(data: inputImage.tiffRepresentation!)

Now you can apply all kinds of filters and other stuff to the image, it's a really powerful tool.

The documentation for CI: https://developer.apple.com/documentation/coreimage

The Filter List: https://developer.apple.com/library/content/documentation/GraphicsImaging/Reference/CoreImageFilterReference/index.html

Here is a simple filter example, you basically initialise a filter and then set the values for it, output it and repeat.

    let yourFilterName = CIFilter(name: "FilterName")
    yourFilterName!.setValue(SomeInputImage, forKey: kCIInputImageKey)
    yourFilterName!.setValue(10, forKey: kCIInputRadiusKey)
    let yourFilterName = yourFilterName!.outputImage

Now you can just convert the output back as NSImage.

    let cgimg = context.createCGImage(yourFilterName!, from: yourFilterName!.extent)
    let processedImage = NSImage(cgImage: cgimg!, size: NSSize(width: 0, height: 0))
Ghosty141
  • 21
  • 7
-5

Try this code it helps.

Swift 3

let theImageView = UIImageView(image: UIImage(named:"foo")!.withRenderingMode(.alwaysTemplate))
theImageView.tintColor = UIColor.red
Jay
  • 686
  • 1
  • 4
  • 16