Xcode Interface Builder & IBDesignables in swift
The problem
You have some fancy visual styling that you know perfectly well how to do in code, but you love using the interface builder and unfortunately you need to set values in code just for your corner radii which won't even be previewed in the interface builder either.
Enter IBDesignable's.
They've been around for quite a few years now, and are actually very straight forward to use. By appling markup and properties to a custom UIView subclass, we can not only implement the functionality, but expose properties that can be set straight from interface builder.
Example 1 (complete swift file)
Let's take a look at a very simple implementation of a UIView subclass that is an IBDesignable, in this case to set the frequently used self.layer.cornerRadius property of a UIView.
import UIKit
@IBDesignable
open class SimpleCornerRadiusView: UIView {
@IBInspectable
public var cornerWidth: CGFloat = 0.0 {
didSet {
self.layer.cornerRadius = cornerWidth
}
}
}
Running through that the following are of note:
* @IBDesignable is what makes tells interface builder in the first place to watch out for custom properties when assigning a custon subclass to our views.
@IBInspectable declares the property that it should be visible to interface builder in the attributes pane. Note that there are certain types that can be used with @IBInspectable: Boolean, Int, CGFloat, string, CGRect, CGPoint, CGSize, UIColor, NSRange*
Try it out
So let's give it a go, open up any basic storyboard and create a UIView subview. Once you've placed it, adjust the class under the 'custom class' properties area to be the name of your class.
Once that is done, click the view properties tab and you'll see a neat surprise.
Just like builtin UIView subclasses (like UITableView or UIImageView) you will see the custom properties that have been defined. In this case we'll set the corner radius property to 10.
Not only are we able to edit the actual values that are applied for these properties in the interface builder, it will render an update and preview the corner radius we have set here.
As you can see the simulator screenshot shows the interface builder rendering to be faithful.
Example 2 (complete swift file)
Now let's do something a little more exotic.
import UIKit
@IBDesignable
open class CustomCornerView: UIView {
var enabledCorners = UIRectCorner()
var maskLayer: CAShapeLayer!
var borderLayer: CAShapeLayer!
var _borderWidth: CGFloat = 0.0
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
createLayers()
}
public override init(frame: CGRect) {
super.init(frame: frame)
createLayers()
}
private func createLayers() {
//Create the mask
maskLayer = CAShapeLayer()
maskLayer.path = UIBezierPath(roundedRect: self.bounds, byRoundingCorners:enabledCorners, cornerRadii: CGSize(width: self.bezelArcSize, height: self.bezelArcSize)).cgPath
maskLayer.frame = self.bounds
layer.mask = maskLayer
borderLayer = CAShapeLayer()
borderLayer.frame = self.bounds
borderLayer.lineWidth = _borderWidth
borderLayer.strokeColor = borderColor.cgColor
borderLayer.fillColor = UIColor.clear.cgColor
borderLayer.path = UIBezierPath(roundedRect: self.bounds, byRoundingCorners:enabledCorners, cornerRadii: CGSize(width: self.bezelArcSize, height: self.bezelArcSize)).cgPath
self.layer.insertSublayer(borderLayer, at: 0)
}
@IBInspectable
public var bezelArcSize: CGFloat = 10.0 {
didSet {
updateMask()
}
}
func addCorner(corner: UIRectCorner) {
enabledCorners.formUnion(corner)
updateMask()
}
func removeCorner(corner: UIRectCorner) {
enabledCorners.subtract(corner)
updateMask()
}
private func updateMask() {
maskLayer.path = UIBezierPath(roundedRect: self.bounds, byRoundingCorners:enabledCorners, cornerRadii: CGSize(width: self.bezelArcSize, height: self.bezelArcSize)).cgPath
maskLayer.frame = self.bounds
borderLayer.path = UIBezierPath(roundedRect: self.bounds, byRoundingCorners:enabledCorners, cornerRadii: CGSize(width: self.bezelArcSize, height: self.bezelArcSize)).cgPath
borderLayer.lineWidth = _borderWidth
borderLayer.strokeColor = borderColor.cgColor
borderLayer.frame = self.bounds
self.maskLayer.setNeedsDisplay()
self.borderLayer.setNeedsDisplay()
self.setNeedsDisplay()
}
@IBInspectable
public var topLeftBezel: Bool = false {
didSet {
topLeftBezel ? addCorner(corner: .topLeft) : removeCorner(corner: .topLeft)
}
}
@IBInspectable
public var topRightBezel: Bool = false {
didSet {
topLeftBezel ? addCorner(corner: .topRight) : removeCorner(corner: .topRight)
}
}
@IBInspectable
public var bottomRightBezel: Bool = false {
didSet {
topLeftBezel ? addCorner(corner: .bottomRight) : removeCorner(corner: .bottomRight)
}
}
@IBInspectable
public var bottomLeftBezel: Bool = false {
didSet {
topLeftBezel ? addCorner(corner: .bottomLeft) : removeCorner(corner: .bottomLeft)
}
}
@IBInspectable
public var borderWidth: CGFloat {
get {
return _borderWidth
}
set {
_borderWidth = newValue
updateMask()
}
}
@IBInspectable
public var borderColor: UIColor = UIColor.clear {
didSet {
updateMask()
}
}
override open var bounds: CGRect {
didSet {
updateMask()
}
}
}
As you can see this is a little more complicated, not only because it has more properties, but also requires usage of instance variables, and also requires some hierarchy to be present on the CGLayer (I found I needed both init methods, even though the withCoder method is used in runtime, the interface builder threw some problems sometimes if I didn't call the required initialisation from there as well.
Here's some effects that such a view can produce now just by toggling the options (captured from the interface builder - design time).