UIAppearance proxy with custom view

Problematic:  I created a custom iOS component to display scores with an icon associated to what it represent. Now I have the need to customize the appearance of this component throughout my application.

My component KWIllustratedLabel is a UILabel subclass that just add an icon on the left and a gradient background with a border, as show in the following.

The KWIllustratedLabel can be used to display two different kind of scores into the application:

  •  Points which represent the user experience, are in yellow with a lightning bolt.
  • Credits  that the user win in the game and can be spent, represented in green with a money bag.

There is also some convenient methods and properties to keep these labels consistent across the application. Here is an extract from the header file.

 

@interface KWIllustratedLabel : UILabel

@property (nonatomic, assign) KWIllustratedLabelType type;
@property (nonatomic, assign) KWIllustratedLabelShadowType shadowType;
@property (nonatomic, assign) KWIllustratedLabelResizingMode resizingMode;
@property (nonatomic, assign) KWIllustratedLabelSize size;
@property (nonatomic, assign) CGFloat minWidth;
@property (nonatomic, assign) CGFloat maxWidth;

- (void)setTextFromScore:(NSNumber *)score;
- (void)setTextFromScoreWithPlusSign:(NSNumber *)score;

@end

Everything is drawn with vectors and can be resized, but that's not the subject here. What we want is to offer more flexibility for the pictograms, so we are providing an alternative artwork for each label type:

  • Points we can have a star instead of the lightning bolt
  • Credits can hide our company logo on the money bag

To achieve this I added a Boolean named useAlternativeIcon that will enable the usage of the new icon depending on the type of the label.

1. Customize properties with UIAppearence

To add support UIAppearance with our custom component we need to:

  1. Subclass UIView (conforms to the UIAppearance protocol)
  2. Expose style attributes as properties and tag them with the UI_APPEARANCE_SELECTOR macro

My class is a UILabel subclass so it also inherit from UIView, by adding the macro on this new attribute we get UIApparence working for free!

@property (nonatomic, strong) NSNumber *useAlternativeIcon UI_APPEARANCE_SELECTOR;

And in the appearance configuration we can use:

KWIllustratedLabel *illustratedLabelAppearance = [KWIllustratedLabel appearance];
illustratedLabelAppearance.useAlternativeIcon = @YES;

But what if we want to use the alternative artwork only for a specific type of illustrated label. Let's say for the Points but not for the Credits for example. It could be useful to be able to specify a parameter that will define when to apply the appearance.

2. Customize parametrized properties (i.e. use alternative artwork only for a specific type)

The documentation is really brief for this, it just says "To participate in the appearance proxy API, tag your appearance property accessor methods in your header with UI_APPEARANCE_SELECTOR."

Appearance property accessor methods must be of the form:

- (PropertyType)propertyForAxis1:(IntegerType)axis1 axis2:(IntegerType)axis2 axisN:(IntegerType)axisN;
- (void)setProperty:(PropertyType)property forAxis1:(IntegerType)axis1 axis2:(IntegerType)axis2 axisN:(IntegerType)axisN;

I followed this and created the methods as you can see below. But then the documentation does not specify how to implement them and when these methods are called by the framework. After some research I found that the setter is called just before your view is added to the window and layout it's content.

/**
 * Defines the label icon appearance for a specific `KWIllustratedLabelType`.
 * @param illustratedLabelType The type of illustrated label that we want to set the alternative representation for.
 */
- (void)setUseAlternativeIcon:(NSNumber *)useAlternativeIcon forLabelType:(KWIllustratedLabelType)illustratedLabelType UI_APPEARANCE_SELECTOR;

/**
 * Returns a boolean that indicates if the label icon appearance is specific for a given `KWIllustratedLabelType`.
 * @param illustratedLabelType The type of illustrated label that we want to get the alternative representation from.
 * @return A number representing a boolean indicating if the label use the alternative representation or not.
 */
- (NSNumber *)useAlternativeIconForLabelType:(KWIllustratedLabelType)illustratedLabelType UI_APPEARANCE_SELECTOR;

You can just save that value in an NSDictionary for example, so you have the value when you draw your views or if the getter gets called later. The internal implementation is really straight forward. Here is the implementation for the getter/setter with the axis and an internal getter for the property that takes care about the setting provided by the proxy.

- (void)setUseAlternativeIcon:(NSNumber *)alternativeIcon forLabelType:(KWIllustratedLabelType)illustratedLabelType {
    if (!self.appearanceStorage) {
        self.appearanceStorage = [NSMutableDictionary dictionary];
    }
    // Set the appearance for the type inside our appearence storage
    NSString *storageKey = [NSString stringWithFormat:@"icon-%u", illustratedLabelType];
    [self.appearanceStorage setValue:alternativeIcon forKey:storageKey];
}

- (NSNumber *)useAlternativeIconForLabelType:(KWIllustratedLabelType)illustratedLabelType {
    // Return the appearance for the type from our appearence storage
    NSString *storageKey = [NSString stringWithFormat:@"icon-%u", illustratedLabelType];
    return [self.appearanceStorage valueForKey:storageKey];
}

// Private accessor that takes care of proxy value if set
- (NSNumber *)useAlternativeIconUsingAppearance {
    // Use the class value if set, else use the proxy value
    if (useAlternativeIcon) {
        return useAlternativeIcon;
    } else {
        return [self useAlternativeIconForLabelType:self.type];
    }
}

So now we can choose for which type of badge we want to use this setting and it's almost as simple as using the setter.

KWIllustratedLabel *illustratedLabelAppearance = [KWIllustratedLabel appearance];
[illustratedLabelAppearance setUseAlternativeIcon:@YES forLabelType:KWIllustratedLabelTypePoints];

UIAppearance is really powerfull and convinient to customize an application appearance. If you are writing custom components, you should be thinking about the compatibility with this framework.

External Links

http://petersteinberger.com/blog/2013/uiappearance-for-custom-views

http://logicalthought.co/blog/2012/10/8/uiappearance-and-custom-views