Sky Watch

IBDesignable is not enough

I was writing a macOS app in Swift, and I needed several text fields accompanied by those little stepper buttons, like what’s shown below. So I decided to write my own control which wraps the text field and the stepper together.

A bunch of steppers
Figure 1. A bunch of NSStepper’s

The custom control itself was simple enough. It was pretty much just a class containing an NSTextField with a formatter and an NSStepper, and that was it (I added a label later).

@IBDesignable
class NumberStepper: NSControl
{
    private var Text: NSTextField = NSTextField(string: "0")
    private var Stepper: NSStepper = NSStepper()
    // ...

    func initSubControls()
    {
        addSubview(Text)
        addSubview(Stepper)
        // Add constraints and stuff...
    }

    override init(frame frameRect: NSRect)
    {
        super.init(frame: frameRect)
        initSubControls()
    }

    required init?(coder: NSCoder)
    {
        super.init(coder: coder)
        initSubControls()
    }

    override func draw(_ dirtyRect: NSRect)
    {
        super.draw(dirtyRect)
    }
}

Notice I added an @IBDesignable before the class defintion. This was to say that the interface builder in Xcode should draw this control when I add it to my UI, so that it should show what’s in the control (the text field and the stepper), instead of just a grey box with “custom NSView” written on it. This was not the first time I did this. I had programmed custom views that had complicated draw() behavior, and @IBDesignable worked nicely for them.

But not this time. This was shown in interface builder:

stepper ib wrong

What?!

I consulted the official doc, again, (It took me a while to find, because interestingly enough, this doc is not in Apple’s dev site. It is under help.apple.com.)

  1. Above the class declaration in the implementation file, enter @IBDesignable for Swift and IB_DESIGNABLE for Objective-C.

  2. Enter the code for your custom view’s draw method.

  3. Save the file.

    Interface Builder renders the view in the canvas.

What I did should be all I needed. So what was wrong? Was it yet another Xcode bug? I spent over two hours trying to find out why my code failed, yet all the materials pointed to the same thing: it should work. (BTW, none of the blog posts and guides I read had a link to the official doc. None.) As my last resort, I posted a question on Stack Overflow. Stack Overflow is full of smart and kind-hearted people. Surely one of them would point me to the right direction, … … right…?

Minutes after I posted my question, it was flagged by a guy named matt as a duplicate of this question. “Stack Overflow is truely an amazing place,” I thought. With gratitude, I followed the link. Well that question did contain words like “interface builder”, “subclass”, and “render”, etc., but the issue of that setup (complicated draw()) did not apply to my case. The accepted answer was informative, but again the extra information was not useful for me in particular.

But there must be a reason my question was marked as duplicate. That matt guy is a Stack Overflow guru, and he marked my post so fast and so decisively; surely he knew what he was doing. “The solution must be in one of the answers in that question,” I thought. So I started to tinker with prepareForInterfaceBuilder() and TARGET_INTERFACE_BUILDER. I spent over one hour on it, but nothing worked. Eventually I drew the conclusion that the discussion in the duplicated question had little to do with my situation.

Wow, amazing! Thanks for wasting one hour of my life, matt guy! Furthermore, nobody would be able to answer my question anymore, because you cannot do that on a freakin’ duplicate! I noted that under my question, there was a paragraph,

This question has been asked before and already has an answer. If those answers do not fully address your question, please edit this question to explain how it is different or ask a new question.

At that point I had already edited my question stating why it was not a duplicate. Probably I would just post a new question like suggested. So I essentially copied my question into a new one. Not surprisingly, another guy came and asked me to delete it and wait for my original question to be unmarked, “or do you want to marked as a spammer?” he asked. Fair enough. And now I’m stuck. I could perhaps reword my question and post yet a new one, but my original question perfectly described the problem; there was no point changing it. Did I expect that matt guy to realize his mistake and unflag my question? No. From experience, I knew a hot-headed guy like him who was not so smart would not ever think about consequences of his/her own action, otherwise s/he would not be hot-headed in the first place, by definition.

Welp, that’s fine. It’s not like I had not solved any problem by myself before. It would just take more time.

After some painful research, I eventually stumbled upon this post. Perhaps I was right to ask if it was an Xcode bug (it was not. But it could use more documentation), and should have posted that question instead. Oh wait, no, never mind; it would just be marked as duplicate by matt.

Anyways, it looked like to me that the way interface builder draws custom views was by “importing” the layer of the views into its canvas. All UIViews are layer-based (which is the case for pretty much all the materials online), so that works. NSViews are not layer-based by default, therefore there is nothing to draw by interface builder, hence the empty box. All I needed to do was turning on layers by setting some properties. And since I did not actually need the layers, I should only do that in the context of interface builder (as opposed to running apps in production). Combining all information, the solution was to put this piece

#if TARGET_INTERFACE_BUILDER
    wantsLayer = true
    canDrawSubviewsIntoLayer = true
#endif

into my initSubControls(). And sure enough, it worked. You can find the program here.

stepper works
Figure 2. A working NumberStepper in interface builder. For some reason it is improperly scaled. But that’s detail.

Aaannd as I had expected, one week later, my question on Stack Overflow was still flagged. So I deleted my question.