Close modal

Blog Post

Xcode Interface Builder & IBDesignables in swift

Development
Fri 04 August 2017
0 Comments


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.

Image

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.

Image

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.

Image

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).

Image


Comments !