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 IBInspectable
var style: UICheckboxStyle {
return self.filled ? .GrayedOut : .OpenCircle
}
override func touchesEnded(touches: NSSet, withEvent event: UIEvent) {
if (self.touchInside) {
self.checked = !self.checked
}
super.touchesEnded(touches, withEvent: event)
}
override func drawRect(rect: CGRect) {
super.drawRect(rect)
let context = UIGraphicsGetCurrentContext()
let fillColor = currentFillColor()
let shadowColor = UIColor.blackColor()
let shadowOffset = CGSizeMake(0.1, -0.1)
let shadowBlurRadius: CGFloat = 2.5
let frame = self.bounds
let group = CGRectMake(CGRectGetMinX(frame) + 3, CGRectGetMinY(frame) + 3, CGRectGetWidth(frame) - 6, CGRectGetHeight(frame) - 6)
let ovalX = CGRectGetMinX(group) + floor(CGRectGetWidth(group) * 0.00000 + 0.5)
let ovalY = CGRectGetMinY(group) + floor(CGRectGetHeight(group) * 0.00000 + 0.5)
let ovalW = floor(CGRectGetWidth(group) * 1.00000 + 0.5) - floor(CGRectGetWidth(group) * 0.00000 + 0.5)
let ovalH = floor(CGRectGetHeight(group) * 1.00000 + 0.5) - floor(CGRectGetHeight(group) * 0.00000 + 0.5)
let ovalRect = CGRectMake(ovalX, ovalY, ovalW, ovalH)
let ovalPath = UIBezierPath(ovalInRect: ovalRect)
CGContextSaveGState(context)
CGContextSetShadowWithColor(context, shadowOffset, shadowBlurRadius, shadowColor.CGColor)
fillColor.setFill()
ovalPath.fill()
CGContextRestoreGState(context)
self.borderColor.setStroke()
ovalPath.lineWidth = self.borderWidth
ovalPath.stroke()
if self.checked || self.filled {
let bezierPath = UIBezierPath()
let startX = CGRectGetMinX(group) + 0.27083 * CGRectGetWidth(group)
let startY = CGRectGetMinY(group) + 0.54167 * CGRectGetHeight(group)
let startPoint = CGPointMake(startX, startY)
let midX = CGRectGetMinX(group) + 0.41667 * CGRectGetWidth(group)
let midY = CGRectGetMinY(group) + 0.68750 * CGRectGetHeight(group)
let midPoint = CGPointMake(midX, midY)
let endX = CGRectGetMinX(group) + 0.75000 * CGRectGetWidth(group)
let endY = CGRectGetMinY(group) + 0.35417 * CGRectGetHeight(group)
let endPoint = CGPointMake(endX, endY)
bezierPath.moveToPoint(startPoint)
bezierPath.addLineToPoint(midPoint)
bezierPath.addLineToPoint(endPoint)
bezierPath.lineCapStyle = kCGLineCapSquare
self.checkmarkColor.setStroke()
bezierPath.lineWidth = self.checkmarkSize;
bezierPath.stroke()
}
}
private func currentFillColor() -> UIColor {
if self.checked {
return self.checkedFillColor
} else if self.filled {
return self.uncheckedFillColor
} else {
return UIColor.clearColor()
}
}
}
Here's some screenshots of the @IBInspectable
properties at work:
There's definitely still some room for improvement. I'm far from a master with UIGraphics
stuff... this project involved some copypasta...
More problematic, is that right now this is purely a view. That is, tapping it wouldn't actually changed the checked
status. For now, you'd have to layer a button over it and hook that up manually. Eventually, for completeness, I want this to be more built-in (though I've not researched quite how to make this work yet). The focus on this project was mostly a checkbox view that was designable in interface builder.
Turning this into a workable button that changes its state and can be hooked up via an IBAction
was surprisingly simple. All I had to do was change from a UIView
subclass to UIControl
, and add the touchesEnded
method (which I just edited in to this post).
UIButton
on a view, and threeUITextView
's experience the same behavior, frame set to X:0,Y:0,H:0,W:0 \$\endgroup\$drawRect
and use that. I'm not sure if I can actually lock it in to 1:1 on IB though. \$\endgroup\$