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.
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:
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.)
Above the class declaration in the implementation file, enter
@IBDesignable
for Swift andIB_DESIGNABLE
for Objective-C.…
Enter the code for your custom view’s draw method.
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 UIView
s are layer-based (which is the case for pretty
much all the materials online), so that works. NSView
s 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.
Aaannd as I had expected, one week later, my question on Stack Overflow was still flagged. So I deleted my question.