Colouring buttons

(macOS 10.11; Xcode 8.0)

This post deals with colouring buttons. Some of the techniques described are against Apple’s InterfaceDesign guidelines; others are simply against the rules of good taste. This post is an exploration of what is possible and will provide a handy reference in the future if I ever think ‘oh, I wish I could have a coloured button’.

Please bear in mind that in Apple design, the default button – if there is one – gets highlighted. This is a design feature and resembles a button coloured blue, which might give a hint why Apple does not want you to easily add colour to a button.

I will use the following states:
bordered buttons, both on and off
unbordered (off only)

A note on operating systems:

I first wrote this post either on macOS 10.9 or 10.10, and while most of the buttons still show the same behaviour, there are some changes, so I have created all of the examples again for the updated version of this post.

Bordered buttons, off, 10.9/10.10 vs. 10.11

borderedbackground-2015-06-1-16-06.jpeg ScreenShot2016-12-30at19.43.51-2015-06-1-16-06-1.png

0) Setup:
Create a new applet. In the storyboard, drag some buttons into the , create an IBOutlet for every button and create a variable to hold all buttons, so you can loop over them and apply changes to all. (‘lazy’ because the outlets are not set when the class is instantiated.) I’m using the buttons in the Object library as they appear for a storyboard project.


[...] @IBOutlet weak var checkBtn: NSButton!

lazy var allButtons: [NSButton] = [self.button, self.textureRoundedBtn, self.gradientBtn, self.roundRectBtn, self.recessedBtn, self.inlineBtn, self.checkBtn, self.radioBtn, self.squareBtn, self.roundBtn, self.bevelBtn]

ScreenShot2016-12-30at19.41.49-2015-06-1-16-06-1.png

Expand the check and radio buttons a little, since the extra drawing makes it hard to see what they are doing otherwise, and set all of them (other than the check and radio buttons) to ‘on off’.

Below the cut lives a very long list of various ways of colouring buttons, all with screenshots

Colouring the background

1) Add the following to viewWillAppear:

for button in allButtons {

button.state = NSOffState
button.isBordered = true

button.wantsLayer = true
let blue = NSColor.blue.cgColor
button.layer?.backgroundColor = blue

}

NSColor.blue.cgColor is the cgColor property of an NSColor, cgLayers cannot deal with NSColor directly.
button.state (the other is NSOffState) and isBordered allow you to quickly loop over the various examples.

Note the layer?: layer! caused a crash when I first ran the app, though it worked ok on subsequent builds. In any case, it’s good form to not force unwrap your optionals.


If you put this code in viewDidLoad you may need to run the app several times before the layer-based changes show up, so I found it vital to override viewWillAppear instead.

a) bordered buttons:
ScreenShot2016-12-30at19.43.51-2015-06-1-16-06-1.png

b) bordered buttons in their ‘on’ state

ScreenShot2016-12-30at19.45.32-2015-06-1-16-06-1.png

c) unbordered buttons:
ScreenShot2016-12-30at19.47.02-2015-06-1-16-06-1.png
The buttonness of buttons does not go away with their unbordered state – they still respond to mouseclicks – but the visual feedback of ‘you have pressed a button’ vanishes, so there’s no point in providing the ‘on’ state here.

1d) For completion’s sake, you can get the same effect by creating a subclass of NSButton and overriding drawRect(dirtyRect: NSRect) instead:

class DrawAnythingButton: NSButton {

override func draw(_ dirtyRect: NSRect) {
NSColor.yellow.set()
NSBezierPath.fill(self.bounds)
super.draw(dirtyRect)
}

}
ScreenShot2016-12-30at19.53.58-2015-06-1-16-06-1.png

Note that the order of calls becomes extremely important here – if you call super.draw(dirtyRect) first – as the template suggests – you will overpaint all of the nice button-ness of your buttons and get the following:
ScreenShot2016-12-30at19.52.20-2015-06-1-16-06-1.png

borderColor

Return all buttons back to the NSButton class.

2) The first example uses the ‘backgroundColor’ property of the button’s layer; this example uses borderColor

button.layer?.borderColor = red
button.layer?.borderWidth = 2

a) bordered buttons

ScreenShot2016-12-30at20.05.20-2015-06-1-16-06-1.png

b) bordered buttons, all in their ‘on’ state:

ScreenShot2016-12-30at20.07.05-2015-06-1-16-06-1.png
c) unbordered buttons
ScreenShot2016-12-30at20.17.46-2015-06-1-16-06-1.png

3) Apple is making various noises about depleting, soft or otherwise, NSCell and its derivatives. They are already strongly discouraging cell-based tables (these still work but should not be used in production code); they have depleted NSMatrix (it still works, but you’re not supposed to use it because they haven’t fully deprecated it _yet_). So use the following NSButtonCell based code with extreme care:

let myCell = button.cell as! NSButtonCell
myCell.backgroundColor = NSColor.green

a) mostly bordered buttons with button.wantsLayer = true

ScreenShot2016-12-30at20.20.30-2015-06-1-16-06-1.png

If I hadn’t known this code was working, I would have discarded it as an option out of hand.

b) unbordered buttons and wantsLayer set to false

ScreenShot2016-12-30at20.22.04-2015-06-1-16-06-1.png
For comparison, this is what the same settings looked like in the previous version:

cellunbordered-2015-06-1-16-06.jpeg

The long and short of this is that trying to colour the background of a button by these means is not overly useful.

Layer Effects with CoreImage content filters

So let’s try something else: using a layer effect on the backing layer. You can set this in InterfaceBuilder (eventually I will supply the code, but right now that’s not a priority)
[I found this via https://parmanoir.com/Changing_the_background_color_of_an_NSButton – this had not been on my radar at all] using CALayer/Backing Layer, with screenshots

Below, for comparison, the colour I am using: ‘tangerine’ from Apple’s crayon colour picker.
ScreenShot2016-12-30at20.38.28-2015-06-1-16-06-1.png
4) backing layer, content filter: colorMonochrome,)
a) bordered

ScreenShot2016-12-30at20.45.34-2015-06-1-16-06-1.png

b) bordered, NSOnState
ScreenShot2016-12-30at20.47.18-2015-06-1-16-06-1.png

c) unbordered (off/on)
ScreenShot2016-12-30at20.49.35-2015-06-1-16-06-1.pngScreenShot2016-12-30at20.48.21-2015-06-1-16-06-1.png
(I’m giving up on this branch: with layers, unbordered obviously does not work for the majority of buttons. Check boxes and radio buttons, on the other hand, may not be a hundred percent useful yet – there’s a considerable deviation from the colour I’ve used to the colour displayed – but I can see myself using a layer-based filter effect at some point.)

5a) And since the backing layer is almost there, here’s one with a white balance filter (‘WhitePoint adjust’); and the balance colour set to tangerine:

ScreenShot2016-12-30at21.00.31-2015-06-1-16-06-1.png

b) whitePoint Adjust, bordered buttons, all on

ScreenShot2016-12-30at21.01.46-2015-06-1-16-06-1.png

Slightly better. There are a couple of useful states in here, but overall I found the button colouring experience somewhat disappointing.

So. Images. Images should work, shouldn’t they? Images are part of a button’s interface in IB, and while you can certainly put an icon on a button, why shouldn’t you be able to use a full blown image covering the whole of the button?

Images

Purple buttons, lots of
6) Delete all layer effects and add two images to your asset catalogue: purple.jpg and purpleHighlight.jpg
purple-2015-06-1-16-06.jpegpurpleHighlight-2015-06-1-16-06.jpeg

set each button’s image to purple, the alternate image to purpleHighlight, and the scaling to .scaleAxesIndependently (you can do that in IB, but this is quicker; layer or not makes no visible difference):

ScreenShot2016-12-30at21.15.45-2015-06-1-16-06-1.png

6a) bordered buttons
ScreenShot2016-12-30at21.13.19-2015-06-1-16-06-1.png
b) bordered buttons, NSOnState

ScreenShot2016-12-30at21.17.58-2015-06-1-16-06-1.png

Apple? This ‘famous for graphical interfaces’ thing? I think we need to talk.

Remove those images. You do not want those images. Those images do not improve your interface.

7) We still want purple buttons. Obviously, the way to go is a PurpleButton class

class PurpleButton: NSButton {

override var wantsUpdateLayer:Bool{
return true
}

override func updateLayer() {
if self.state == NSOnState{
self.layer?.contents = NSImage(named: "purpleHighlight")
}
else {
self.layer?.contents =NSImage(named: "purple") }
}
}

(Here’s a cool thing: when I copied my code with image literals, the system pasted self.layer?.contents = #imageLiteral(resourceName: "purple"), which remains human-readable even in a plain text environment. Bravo Apple.)

This is a subclass of NSButton that updates its layer (and thus you must set wantsLayer to true; I do this in the same place as before, but you could also override wantsUpdateLayer, only it needs to have a getter _and_ a setter, otherwise the compiler complains and I was too lazy to implement this properly.) Set the class of all your buttons to PurpleButton.

7a) PurpleButtons, bordered
ScreenShot2016-12-30at21.25.47-2015-06-1-16-06-1.png

b) bordered, NSOnState

ScreenShot2016-12-30at21.26.43-2015-06-1-16-06-1.png

If you start with a button-shaped image with transparency around the edges, you could almost, I don’t know, have coloured button or something. But that only works if you *can* lovingly handcraft your buttons.

If you want a more dynamic way of creating images to draw onto your buttons, you can compose an NSImage and use *that* for your button’s image. Here, I’m replacing the image(named: “purpleHighlight”) with a single-colour NSImage created from NSColor.cyanColor():

c) Turn all your buttons into NSButtons again, add

button.layer?.contents = drawDynamicImage()

to the enumeration in viewWillAppear, and add the following function to the viewController class:

func drawDynamicImage() -> NSImage{
let mySize = NSMakeSize(200, 200)
let tempImage = NSImage(size: mySize)
tempImage.lockFocus()
NSColor.cyan.drawSwatch(in: NSMakeRect(0, 0, mySize.width, mySize.height))
tempImage.unlockFocus()
return tempImage
}

NSButton, bordered, NSOffState/NSOnState

ScreenShot2016-12-30at21.35.21-2015-06-1-16-06-1.pngScreenShot2016-12-30at21.49.55-2015-06-1-16-06-1.png

Wait, what? That’s not what I expected.

Create a CyanButton class, and provide the same drawDynamicImage function.

class CyanButton: NSButton {

override var wantsUpdateLayer:Bool{
return true
}

override func updateLayer() {
if self.state == NSOnState{
self.layer?.contents = drawDynamicImage()
}
else {
self.layer?.contents = #imageLiteral(resourceName: “purple”)
}
}

CyanButton, bordered, NSOnState

ScreenShot2016-12-30at21.43.42-2015-06-1-16-06-1.png

It appears that trying to set the button’s layer contents in ViewWillAppear is too late; I would not have thought that the difference between the two locations of what is essentiatlly the same code would make that much of a difference.

tl;dr:

All of the methods described here want adaptive measures; none of them work right out of the box. Button-shaped images with part transparency seem to be the best bet; and one might to adjust the colour/alpha value for on and off states. As it stands, I don’t find any of these truly satisfactory, though the coloured check and radio button using the whiteBalance content filter seem promising.