Categories
interface-builder ios objective-c uibutton xcode6

UIButton with IB_DESIGNABLE throws runtime attribute warning and does not render in Interface Builder

I’m running Xcode 6.1 and I have been using IB_DESIGNABLE with IBInspectable for quite a few projects already but all of the sudden it just doesn’t work anymore. I have created subclassed buttons that arrange the image and the title vertically centred above each other and enable the user to set a border width and color through IB with IBInspectable.

The following warning is logged and there is no preview available of my code in drawRect:

warning: IB Designables: Ignoring user defined runtime attribute for key path "spacingBetweenLabelAndImage" on instance of "UIButton". Hit an exception when attempting to set its value: [<UIButton 0x7f9278591260> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key spacingBetweenLabelAndImage.

Still, runtime it renders like I intended it to render and it also honours that same custom spacing I’ve added through IB.

Here’s the code of the menubutton that rearranges the button and the title:

#import "HamburgerButton.h"
IB_DESIGNABLE
@interface HamburgerImageButton : HamburgerButton
@property IBInspectable CGFloat spacingBetweenLabelAndImage;
@end

Implementation:

#import "HamburgerImageButton.h"
@implementation HamburgerImageButton
- (void)drawRect:(CGRect)rect {
[super drawRect:rect];
CGSize imageSize = self.imageView.frame.size;
CGSize titleSize = self.titleLabel.frame.size;
// Move the label left and the image right by half the width
CGFloat leftInset = titleSize.width / 2;
CGFloat rightInset = imageSize.width / 2;
CGFloat halfSpacing = self.spacingBetweenLabelAndImage == 0 ? 0 : self.spacingBetweenLabelAndImage / 2;
CGFloat topInset = imageSize.height / 2 + halfSpacing;
CGFloat bottomInset = titleSize.height / 2 + halfSpacing;
UIEdgeInsets imageInsets = UIEdgeInsetsMake(-bottomInset, leftInset, bottomInset, -leftInset);
UIEdgeInsets titleInsets = UIEdgeInsetsMake(topInset, -rightInset, -topInset, rightInset);
self.imageEdgeInsets = imageInsets;
self.titleEdgeInsets = titleInsets;
}
@end

You’ve probably noticed it inherits HamburgerButton. This basic hamburger button does not have an image and it only draws the border around the button. This basic hamburger button has exactly the same problem: it does not draw it’s border in drawRect in IB and it has the same type of errors. Here’s that code for sake of completeness:

#import <UIKit/UIKit.h>
IB_DESIGNABLE
@interface HamburgerButton : UIButton
@property IBInspectable CGFloat borderWidth;
@property IBInspectable UIColor *borderColor;
@end

Implementation:

#import "HamburgerButton.h"
#import <QuartzCore/QuartzCore.h>
@implementation HamburgerButton
- (void)copyInspectables {
self.layer.borderWidth = self.borderWidth;
self.layer.borderColor = self.borderColor.CGColor;
}
- (void)drawRect:(CGRect)rect {
[self copyInspectables];
}
@end

I don’t really understand why it just throws a warning and nothing else. I didn’t really change what I did. I’ve checked the storyboard, it’s iOS 7 and up, meant to run in Xcode 6 (latest).

It complains about not being able to find that value on UIButton and that’s a bit weird because I’ve subclassed it.


Update:
So I changed everything around and it worked. Now it craps out again, without changing anything else. I think there’s a bug in Xcode 6.1… :/

I have been working extensively with Live Views since it’s introductions and like a lot of new features in Xcode initially, it has some flaws. Still I like it a lot and I hope that it will be improved in newer versions.

Cause:

Live Views work with @property properties that get a special IB_Designable tag that will give the UI class extra attributes it can approach via User Defined Runtime Attributes that will be set through the Attributes Inspector. You will see that all those properties also exist in the Identity Inspector. The root cause of the problem is that it cannot map those User Defined Runtime Attributes anymore to exposed properties.

As soon as you open up a Storyboard or xib that uses IB_Designable, Xcode will recompile the screen at every change if you have “Automatically Refresh Views” turned on. Since your code influences your UI as well this doesn’t have to be a change in Interface Builder per se.

With simpler projects on faster machines this works pretty well the majority of the time. With larger projects that also have more IB_Designable the CPU simply doesn’t have enough capacity to keep up with you and you will get a sort of time-out error while rendering the UI. This is the cause of the “warning: IB Designables: Ignoring user defined runtime attribute for key path “spacingBetweenLabelAndImage” on instance of “UIButton”. Hit an exception when attempting to set its value: [ setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key spacingBetweenLabelAndImage.” error.

It will also not update your UI anymore with your custom code. This is not a problem when you run it though, because at run time it will reflect those changes.

Another cause can be that your code simply doesn’t compile when it tries to compile in the background. This is inevitable when you are working on code so that triggers it frequently as well.

Solution:

We need to make sure that your custom view class compiles again so the properties can be set successfully via Interface Builder

  • While in a storyboard, go to “Editor” in the top bar menu
  • deselect “Automatically refresh views” if it was still turned on
  • Close all your storyboard tabs
  • Rebuild the project
  • Fix all build errors if any
  • Re-open your storyboard.
  • Go to “Editor” again and click “Refresh All Views”

The warnings should be gone and if it worked before you will see your custom view code again as well.

After a comment below by sunkas: it’s always a very good idea to clean your solution thoroughly in case of any weird problems that persist.