User Defined runtime attributes are hidden gem of Xcode Interface Builder.Available since Xcode4 and iOS5.They provide availability to configure properties of a view that you would otherwise be unable to configure in Interface builder.
As an advocate for the separation of concerns, I believe we should do in Interface Builder as much interface configuration as possible. Although runtime attributes are often overlooked, they can lead to much cleaner view controller code – something we can all benefit from.
As an advocate for the separation of concerns, I believe we should do in Interface Builder as much interface configuration as possible. Although runtime attributes are often overlooked, they can lead to much cleaner view controller code – something we can all benefit from.
Usage
User defined runtime attributes are set from the Identity Inspector tab in Interface Builder’s utilities, as shown below.
They are defined as a set of key path/value pairs where the view must be Key Value Coding-compliant for each of the given key paths.
Attribute Types
Below is a list of the available attribute types and the corresponding property type.
- Boolean – BOOL
- Number – NSNumber * or any numeric scalar, e.g. NSInteger
- String – NSString *
- Point – CGPoint
- Size – CGSize
- Rect – CGRect
- Range – NSRange
- Color – UIColor *
There are also the following special types:
- Nil – this special type doesn’t allow you to set a value, it is just a way of specifying that the property should be set to nil
- Localized String – the value here is a key to look up in the strings file for the current locale, the property is set to the corresponding localized string
Simple Examples
Configuring a view’s underlying CALayer is not possible in Interface Builder’s Attributes Inspector. But with runtime attributes we can easily give the view a border and rounded corners by setting thelayer.borderWidth and layer.cornerRadius key paths, as shown below.
Configuring a custom control has never been easier! For example, if you create your own range slider (like a UISlider but with two thumbs to specify a minimum and maximum value in a range), and add it to a view in Interface Builder, then you can configure it as follows to avoid cluttering the view controller with unnecessary view configuration code.
Converting From Other Types
Any attribute type can be used to configure a property of another type as long as the type is represented in the same way. For example Point and Size can be used interchangeably, but they can also be used to represent a property of type CGVector, because the underlying structure is the same (i.e. two floating point numbers).
But you can also use another little trick to configure a property of a type otherwise unsupported by Interface Builder. For example, continuing on from configuring a view’s underlying CALayer, you may wish to set the border colour or shadow colour. This isn’t directly supported using runtime attributes, because these properties are of type CGColorRef. But consider the following example:
In order to make CALayer KVC-compliant for the property borderColorFromUIColor, simply implement the setter for the property. This can be done in a category on CALayer as follows:
@implementation CALayer (Additions)
- (void)setBorderColorFromUIColor:(UIColor *)color
{
self.borderColor = color.CGColor;
}
@end
Convert From String
It is often useful to convert from a string to another type. For example, configuring a property of type UIEdgeInsets isn’t directly supported by runtime attributes, but consider the following category:
@implementation UIScrollView (Additions)
- (void)setContentInsetFromString:(NSString *)contentInsetString
{
self.contentInset = UIEdgeInsetsFromString(contentInsetString);
}
@end
It’s particularly easy to convert from a string to UIEdgeInsets type, because of the niceUIEdgeInsetsFromString method provided by UIKit. But you could define your own method to convert from a string to any other type you can think of. Get creative!
More Advanced Uses
Often it is useful to be able to set some attributes of a view where the attribute doesn’t have a corresponding property on the view.
Imagine a scenario where you have various UIView views within a view controller. You want to use UIKit Dynamics to make each view drop to the bottom of the screen and bounce. And you want to define how high it should bounce in Interface Builder, because after all, you are building the interface!
Consider the following setup for runtime attributes on a UIView.
UIView is not KVC-compliant for the key path elasticity. And an exception will be raised if you run the app now. But you can change this by defining a category on UIView with the property elasticity (see category properties).
Now you can use this property when you configure the UIDynamicItemBehavior in your view controller. This is nice because it means you don’t have to have logic in your view controller for each view individually. Your view controller only needs to know about a collection of views (i.e. IBOutletCollection). The specific configuration of each is done in Interface Builder, where it belongs.
for (UILabel *label in self.labels) {
UIDynamicItemBehavior *itemBehavior = [[UIDynamicItemBehavior alloc] initWithItems:@[label]];
[self.animator addBehavior:itemBehavior];
itemBehavior.elasticity = label.elasticity;
}
Considerations
I think this is the right way to configure a user interface. Given that we have Interface Builder, we should use it to its full potential. But… there’s always a “but” isn’t there?
Many people wouldn’t think to look in Interface Builder at the user defined runtime attributes, so they could be left very puzzled if they come to maintain your code at a later date. They could spend hours trying to figure out why a view has a border, and resort to overriding it in the view controller just to remove it.
So like anything of this nature — and by that I mean a more-advanced use of a programming language or development environment — we should use it with caution. Use it only if it is necessary and simplifies logic elsewhere in your code.
Setting up a view in a view controller isn’t necessarily messy. The most valid reason for using user-defined runtime attributes is to configure a view differently whether the Interface Builder file is for iPhone or iPad. Because you already have a separate nib or storyboard file specifically for each device, that should be the place that you do any device-specific configuration.
There are plenty of other uses for runtime attributes. I’d be keen to hear any creative examples you have thought of and I’d be happy to discuss the pros and cons at greater length, so please comment!
0 comments:
Post a Comment