HiveBrain v1.2.0
Get Started
← Back to all entries
patternswiftMinor

IBDesignable UICheckbox

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
ibdesignableuicheckboxstackoverflow

Problem

A more up-to-date version of this control can be found on GitHub.

One UI control that has always been mysteriously missing from Xcode's interface builder is some sort of checkbox. UISwitch is available, but this isn't always quite the feel you might want. It's particularly mysterious that any sort of check box is missing given that there are check boxes used somewhat predominately throughout Apple's own preloaded apps (Camera Roll is first thing that comes to mind).

As such, I decided to implement a UICheckbox... and one that can be designed in interface builder!

```
//
// UICheckbox.swift
// UIStuff
//
// Created by Nick Griffith on 11/6/14.
// Copyright (c) 2014 nhg. All rights reserved.
//

import UIKit

enum UICheckboxStyle {
case OpenCircle, GrayedOut
}

@IBDesignable class UICheckbox: UIControl {
@IBInspectable var checked: Bool = false {
didSet {
self.setNeedsDisplay()
}
}

@IBInspectable var checkmarkSize: CGFloat = 5.0 {
didSet {
self.setNeedsDisplay()
}
}

@IBInspectable var checkmarkColor: UIColor = UIColor.whiteColor() {
didSet {
self.setNeedsDisplay()
}
}

@IBInspectable var filled: Bool = true {
didSet {
self.setNeedsDisplay()
}
}

@IBInspectable var checkedFillColor: UIColor = UIColor(red: 0.078, green: 0.435, blue: 0.875, alpha: 1.0) {
didSet {
self.setNeedsDisplay()
}
}

@IBInspectable var uncheckedFillColor: UIColor = UIColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 0.3) {
didSet {
self.setNeedsDisplay()
}
}

@IBInspectable var borderColor: UIColor = UIColor.whiteColor() {
didSet {
self.setNeedsDisplay()
}
}

@IBInspectable var borderWidth: CGFloat = 1.0 {
didSet {
self.setNeedsDisplay()
}
}

// custom enums, unfortunately, cannot be IBInspectabl

Solution

In playing around with this, I noticed a few things I didn't particularly like:

This picture represents the first problem. Setting the background color fills in the entire rectangle, but should probably only fill in the circle. We can fix this as such:

Add a private variable to hold the value of our circle's background:

private var _backgroundColor: UIColor = UIColor.clearColor() {
    didSet {
        self.setNeedsDisplay()
    }
}


Now override the default property, backgroundColor:

override var backgroundColor: UIColor? {
    set(newColor) {
        super.backgroundColor = UIColor.clearColor()
        if let backColor = newColor {
            self._backgroundColor = backColor
        } else {
            self._backgroundColor = UIColor.clearColor()
        }
    }
    get {
        return self._backgroundColor
    }
}


The next problem is represented here:

This doesn't really look great. I don't know if there's a way to enforce a 1:1 ratio for the rect in interface builder itself (like some Apple UI elements are able to enforce a height), however, we can add this code to drawRect to help:

var frame = rect
if frame.size.width != frame.size.height {
    let shortestSide = min(frame.size.width, frame.size.height)
    let longestSide = max(frame.size.width, frame.size.height)

    let originY = (longestSide - frame.size.width) / 2
    let originX = (longestSide - frame.size.height) / 2

    frame = CGRectMake(originX, originY, shortestSide, shortestSide)
}

let group = CGRectMake(CGRectGetMinX(frame) + 3, CGRectGetMinY(frame) + 3, CGRectGetWidth(frame) - 6, CGRectGetHeight(frame) - 6)


Now we're calculating a square within the rectangle and centering it, giving the following result:

The third problem is the inability to have a transparent checkmark, which might be desirable:

As you can see here, Checked is set to On, but no checkmark appears because the checkmark color is set to Clear. However, we can actually fix this. If we first stroke the checkmark path with the clear blend, then restroke with the normal blend, transparent/translucent checkmark colors will let the color behind our object show through. Here's the code:

bezierPath.lineWidth = self.checkmarkSize            
self.checkmarkColor.setStroke()

CGContextSetBlendMode(context, kCGBlendModeClear)
bezierPath.stroke()
CGContextSetBlendMode(context, kCGBlendModeNormal)
bezierPath.stroke()


And here's the result:

With a particularly large border, we run into trouble due to some magic numbers:

Now, why anyone would want a checkbox anywhere near this size is completely beyond me, but all the same, if we're going to off a variable for a custom border size, it should look decent no matter the selected size (within reason... not much we can do if the object is 200px by 200px and a border size of 200px is selected).

If we change the code that calculates the group rect to something more like this:

let groupX = CGRectGetMinX(frame) + (self.borderWidth / 2)
let groupY = CGRectGetMinY(frame) + (self.borderWidth / 2)
let groupW = CGRectGetWidth(frame) - self.borderWidth
let groupH = CGRectGetHeight(frame) - self.borderWidth

let group = CGRectMake(groupX, groupY, groupW, groupH)


Then we get a much nicer looking result:

The sizing rect makes the object still appear to have slightly flat edges, but when run in the application, it's actually a quite smooth circle.

(And would look better still on an actual device which has a much higher pixel density than my computer)

Code Snippets

private var _backgroundColor: UIColor = UIColor.clearColor() {
    didSet {
        self.setNeedsDisplay()
    }
}
override var backgroundColor: UIColor? {
    set(newColor) {
        super.backgroundColor = UIColor.clearColor()
        if let backColor = newColor {
            self._backgroundColor = backColor
        } else {
            self._backgroundColor = UIColor.clearColor()
        }
    }
    get {
        return self._backgroundColor
    }
}
var frame = rect
if frame.size.width != frame.size.height {
    let shortestSide = min(frame.size.width, frame.size.height)
    let longestSide = max(frame.size.width, frame.size.height)

    let originY = (longestSide - frame.size.width) / 2
    let originX = (longestSide - frame.size.height) / 2

    frame = CGRectMake(originX, originY, shortestSide, shortestSide)
}

let group = CGRectMake(CGRectGetMinX(frame) + 3, CGRectGetMinY(frame) + 3, CGRectGetWidth(frame) - 6, CGRectGetHeight(frame) - 6)
bezierPath.lineWidth = self.checkmarkSize            
self.checkmarkColor.setStroke()

CGContextSetBlendMode(context, kCGBlendModeClear)
bezierPath.stroke()
CGContextSetBlendMode(context, kCGBlendModeNormal)
bezierPath.stroke()
let groupX = CGRectGetMinX(frame) + (self.borderWidth / 2)
let groupY = CGRectGetMinY(frame) + (self.borderWidth / 2)
let groupW = CGRectGetWidth(frame) - self.borderWidth
let groupH = CGRectGetHeight(frame) - self.borderWidth

let group = CGRectMake(groupX, groupY, groupW, groupH)

Context

StackExchange Code Review Q#69123, answer score: 8

Revisions (0)

No revisions yet.