Here at Coursera, every new line of code we write for iOS is in Swift. As a result, I’ve written a few custom controls lately in Swift using Auto Layout and iOS 8’s new IBDesignable/IBInspectable attributes which are *supposed* to live render custom views in Storyboard files. I’ve seen a simple example setting up the new Interface Builder (IB) attributes and a custom control rewritten using Swift, but nothing written from scratch with all these technologies in mind. Today, I’m going to walk through the strategies I used when writing the custom scrubber we recently open sourced from Coursera. Before we get started, it’s worth setting up your project using the simple custom control example by WeHeartSwift I mentioned earlier since those IB attributes require certain project configuration.
Creating the ScrubberBar with Auto Layout
As you saw above, the scrubber we want to build today will look something like this:
There are a lot of components here, so let’s start by creating the ScrubberBar as a subclass of UIView. Since we are considering Auto Layout from the beginning, there are three things to immediately think about:
required init(coder aDecoder: NSCoder) {
super.init(code: aDecoder)
setTranslatesAutoresizingMaskIntoConstraints(false)
}
2. Override the intrinsicContentSize to specify what size this component would like to be. I typically define these intrinsic values as private properties, but for now I’ll simply specify some reasonable values.
override func intrinsicContentSize() -> CGSize {
return CGSizeMake(100, 70)
}
3. Indicate to other views that this view requires auto-layout.
override class func requiresConstraintBasedLayout() -> Bool {
return true
}
Finally, we need to set the corner radius for the ScrubberBar’s view to get those rounded corners. In order to do this while respecting Auto Layout we will override layoutSubviews which gets called when the constraints and frame changes.
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = frame.height/2
}
Establishing View Hierarchy
Now that we have our ScrubberBar component created, we need to think about the view hierarchy for our control. While it makes since for UI aspects like ScrubberEvents and the BufferBar to be laid out by the ScrubberBar other things like the ScrubberElement seem more independent. Therefore, we likely need an overarching container for both ScrubberElement and ScrubberBar components. We’ll call this container something über creative like “ScrubberControl”. We are able to choose such a generic name because frameworks are namespaced in Swift. Therefore, if a name conflict occurs you can refer to the scrubber as <Framework Name>.ScrubberControl.
Now as with the ScrubberBar, this custom view will need the same initializations as before. However, this time the ScrubberControl will also create a ScrubberBar in the init method.
public var scrubberColor: UIColor = UIColor.grayColor()
required public init(coder aDecoder: NSCoder) {
scrubberBar = ScrubberBar(coder: aDecoder)
super.init(coder: aDecoder)
setTranslatesAutoresizingMaskIntoConstraints(false)
addSubview(scrubberBar)
scrubberBar.backgroundColor = scrubberBarColor
setupLayout()
}
You’ve probably noticed we also set the ScrubberBar’s color, this is so we can see the view when its rendered later.
Next we need to use Auto Layout to position the ScrubberBar in our ScrubberControl’s view. For this we’ll implement the setupLayout method above, creating several layout constraints to center the ScrubberBar in our control and then add them to our view.
func setupLayout() {
var constraintsArray = Array<NSObject>()
// Background Bar Constraints
constraintsArray.append(NSLayoutConstraint(item: scrubberBar, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: self, attribute: NSLayoutAttribute.CenterX, multiplier: 1.0, constant: 0.0))
constraintsArray.append(NSLayoutConstraint(item: scrubberBar, attribute: NSLayoutAttribute.CenterY, relatedBy: NSLayoutRelation.Equal, toItem: self, attribute: NSLayoutAttribute.CenterY, multiplier: 1.0, constant: 0.0))
constraintsArray.append(NSLayoutConstraint(item: scrubberBar, attribute: NSLayoutAttribute.Width, relatedBy: NSLayoutRelation.Equal, toItem: self, attribute: NSLayoutAttribute.Width, multiplier: 1.0, constant: 1.0))
constraintsArray.append(NSLayoutConstraint(item: scrubberBar, attribute: NSLayoutAttribute.Height , relatedBy: NSLayoutRelation.Equal, toItem: nil, attribute: NSLayoutAttribute.NotAnAttribute, multiplier: 1.0, constant: 15))
self.addConstraints(constraintsArray)
}
Always make sure to add the subview to the view hierarchy before adding constraints and to always add those constraints on the parent view.
Provided you’ve added your custom control to the view hierarchy through code or storyboard you should be able to run your project and render something like this:
I colored the background of the ScrubberControl orange to show its frame
Making the Control Viewable in Interface Builder
At this point we have a basic control, now let’s make it available our Storyboard. This part requires some project configuration, so if you haven’t been building these classes inside a separate framework then you might want to walk through the simple WeHeartSwift walkthrough I mentioned earlier. First we’ll add the @IBDesignable attribute to our class.
@IBDesignable public class ScrubberControl: UIView {
Then our next step will be making the scrubberBarColor property available for use in the Storyboard. To get this behavior, apply the @IBInspectable attribute to the scrubberBarColor property and change the border color of our scrubber whenever that variable is set.
@IBInspectable var scrubberBarColor: UIColor = UIColor.grayColor() {
didSet {
scrubberBar.backgroundColor = scrubberBarColor
}
}
Now you should be able to change the scrubber bar color in your Storyboard and have that color reflected when you build+run your app.
Why can’t you just view the rendered ScrubberControl in the storyboard with the new value? Well… thats because despite Apple launching the feature to live render custom views it doesn’t really work. Here are my real world experiences using the @IBDesignable attribute with Xcode 6.0.1:
In summary, @IBDesignable is unusable, however @IBInspectable has always reliable.
Handling Touch Input or Animation with Auto Layout
At this point we just have a simple ScrubberBar inside our ScrubberControl, however as we build each new component the steps I described above can be applied repeatedly to design a static layout:
Therefore, I’m going to skip ahead a bit and discuss how we handled user input on the ScrubberElement of our control. The behavior we want for this ScrubberElement is for a drag gesture to update the element’s center, but for that center to never exceed the extent of the scrubber bar.
When handling any user input or animation while using Auto Layout I perform two steps:
Just like previously, the first thing to do is create all constraints within the init method. Next, store any constraints affected by an animation or touch input in a property. For our ScrubberElement the center constraint is the sole aspect that affects our element’s horizontal position. Therefore we will store it in a property so it can be adjusted after the touch gesture completes.
scrubberElementCenterConstraint = NSLayoutConstraint(item: scrubberElement, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: scrubberBar, attribute: NSLayoutAttribute.Left, multiplier: 1.0, constant: centerValue)
Next, when the drag gesture occurs, directly set the center of our element to keep it in step with the touch location.
scrubberElement.center = CGPointMake(calculatedXCoordinate, scrubberElement.center.y)
Lastly, after the touch completes update the constraint with the new values by calling the layoutSubviews method and overriding its implementation.
override public func layoutSubviews() {
super.layoutSubviews()
if let centerConstraint = scrubberElementCenterConstraint {
centerConstraint.constant = scrubberBar.centerValueForItem(scrubberElement.index)
}
…
You may have noticed that I’m not updating constraints in the updateConstraints method (the typical scenario). This is because the centerValueForItem method used above relies on the scrubberBar’s frame. Therefore, the center constant value must be calculated later on in the view’s layout cycle (later than updateConstraints) in order to return a correct frame value.
Closing Thoughts
As you’ve seen, despite how intimidating UIKit can be creating a custom UI component isn’t all that difficult. Simply following a repeatable procedure will lead you down the right path 90% of the time. There is a lot more code than what we walked through in this post so take a look at the custom scrubber control we open sourced for more details. If you want to learn more about the layout life-cycle and using auto-layout with custom controls I suggest reading the objc.io blog’s Advanced Auto Layout Toolbox.