Working with IBDesignable

Part 3


Editor’s Note: This is the final article in a series that aims to help designers get more familiar with working in Xcode. Be sure to read parts 1 and 2 first.

If you’ve gone through the first part of our IBDesignable series, you understand how to create a simple solid-color roundrect which can be customized on the Xcode storyboard–at least when working in iOS. Now we’re going to look at doing the same for Mac.

If you haven’t reviewed our tutorial on creating a customizable solid-color roundrect, go back and check it out now. Otherwise, what follows may seem inscrutable.

Mac Differences

The most immediate differences you’ll notice between our iOS project and the Mac project we’re about to start are the disparities in names of system classes. On iOS, these tend to start with “UI,” whereas on Mac, they usually begin with “NS.” Thus, UIView on iOS becomes NSView on Mac, UIColor becomes NSColor, and so on.

Create the Project and Color Block Class

Launch Xcode. Select File > New > Project. Choose OS X > Application > Cocoa Application. Hit Next. Fill out the product details however you like, but make sure the language is set to Swift. That’s the language we’ll be using. Check “Use Storyboards.”
Create a Mac app project

As with our iOS project, we’ll use a new view subclass as our ColorBlockView.

Select File > New > File. Choose OS X > Source > Cocoa Class. Hit Next. Provide a class name of “ColorBlockView” and set it to be a subclass of NSView. Leave the language as Swift. Hit Next and save the new file in your project’s folder. Xcode will automatically select it in the Project navigator.
Subclass NSView

Add the keyword @IBDesignable just before the class definition.

By default, our subclass includes some code that we won’t use. Delete the override func drawRect… function definition, including its opening and closing curly braces. After these changes, your ColorBlockView will look like this:

import Cocoa
@IBDesignable class ColorBlockView: NSView {

}
The @IBDesignable keyword

Insist on Layers

Previously, we used the UIView’s layer to host customizations like a gradient fill or corner rounding. UIView conveniently includes a layer object “out of the box.” NSView, much less conveniently, does not.

On the Mac we need to alter NSView’s initialization to include a layer, so we’ll be sure that it’s there when we want to make changes to it. “Initialization” refers to the point at which an object is first spun up for use in the app. We can modify this process with a block of code called an initializer.

Initialization in Swift is a complex process full of rules and safety checks. At this point we’re going to take a shortcut. Drop in the following block of code between your class definition curly braces:

required init?(coder: NSCoder) {
super.init(coder: coder)
self.wantsLayer = true
}
We definitely want a layer here

The only part you should worry about understanding right now is the third line — the rest is boilerplate. The line self.wantsLayer = true indicates to Xcode that at runtime our ColorBlockView is going to want a layer, and it’s going to get one. With that settled, we can start customizing our view’s appearance.

As we do so, though, we’ll have to make allowances for Xcode. Xcode isn’t smart enough to recognize that we just guaranteed ColorBlockView will always have a layer. So anytime we ask for a change to layer, we have to allow for the possibility that it might not exist — even though, really, it almost certainly will.

Round Corners

Looking back at our ColorBlockView on iOS, one of the key things we wanted to customize was the view’s cornerRadius. We set up a variable named cornerRounding and then we added:

layer.cornerRadius = cornerRounding

That will almost work on the Mac too, but since it’s conceivable that our ColorBlockView won’t have a layer (it will, really), we have to establish that layer is an “optional.” We do that by appending a question mark:

layer?.cornerRadius = cornerRounding

What we’re saying here is, “If you find a layer, adjust its cornerRadius as follows.” Otherwise, our corner radius adjustment code is almost identical to that which we supplied for iOS. Add this block within the class definition:

@IBInspectable var cornerRounding: CGFloat = 10 {
didSet {
layer?.cornerRadius = cornerRounding
}
}
The cornerRounding variable — layer optional?

Layer Color

The mechanism for customizing the background color is also very similar to what we did before with a couple key differences. As we said, “UIColor” will be replaced with “NSColor”:

@IBInspectable var blockColor: NSColor = NSColor.gray

That’s easy enough. But now when we supply the didSet, we can’t simply declare:

backgroundColor = blockColor

That’s what we did with our iOS project, but NSView doesn’t have a backgroundColor. We have to set backgroundColor on the layer instead:

@IBInspectable var blockColor: NSColor = NSColor.gray {
didSet {
layer?.backgroundColor = blockColor
}
}

(Remember, we need the ? to account for the ambiguous existence of layer.)

However, this still won’t quite work. We’ve set up blockColor as a type of NSColor. But layer is really a “CALayer” type of object (the “CA” stands for “Core Animation”). CALayers won’t accept NSColors. They want color information in a CGColor format instead. (As before, the “CG” prefix refers to the Core Graphics framework.)

That sounds messy, but the fix is actually remarkably simple. We just tell Xcode to perform the CGColor conversion for us:

@IBInspectable var blockColor: NSColor = NSColor.gray {
didSet {
layer?.backgroundColor = blockColor.cgColor
}
}
The blockColor variable with conversion to CGColor

Packaging

Now we have customizable color as well as rounding. At this point, our ColorBlockView will work pretty well on a storyboard. But as with iOS, we want to make sure to cover all the bases. Let’s add the backgroundColor and cornerRadius updating to both the prepareForInterfaceBuilder() and awakeFromNib()functions:

override func prepareForInterfaceBuilder() {
layer?.backgroundColor = blockColor.cgColor
layer?.cornerRadius = cornerRounding
}

override func awakeFromNib() {
layer?.backgroundColor = blockColor.cgColor
layer?.cornerRadius = cornerRounding
}
One function for storyboarding, the other for runtime

Let’s see what we get for our work. Switch to Main.storyboard. Type “custom view” in the Object library search field and drag one to the View Controller (not the Window Controller, which is just a wrapper for the View Controller). Give it the class “ColorBlockView” in the Identity inspector. Switch to the Attributes inspector and play with the Block Color and Corner Rounding.

The working ColorBlockView

Go Crazy

That’s all for IBDesignable and IBInspectable. If you want to go a little further, try creating a GradientBlockView in your Mac project. It works much like it did on iOS — remember to use layer? — with the caveat that gradients run bottom to top on the Mac. You can flip your gradient layer with the following code:

gradientLayer.startPoint = CGPointMake(0.5, 1.0)
gradientLayer.endPoint = CGPointMake(0.5, 0.0)

Give it a shot, and experiment with what other properties can be exposed to Interface Builder in both iOS and Mac projects. You might try border color and border width — borderColor and borderWidth. And those are just the straightforward choices!

(You can download the example project file here.)

Like what you read? Give John Marstall a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.