Nat! bio photo

Nat!

Senior Mull

Twitter Github Twitch

-[UIButton tintColor] is just weird and non-conforming - or class clusters don't mix well with UIAppearance

I figured, that I wanted to use UIAppearance for my code, so that I can easily "skin" my iOS applications. What I wasn't really clear about is how to properly handle default values, because like most recent Apple code, it's just magic. I hate magic.

To test the best way to handle default values for my UIViews, I wanted to test UIAppearance first with the most simple and minimal code possible and go from there. I didn't even make it...

Step 1: Create a new iOS application with a single view

Step 2: Enter the following lines in the app delegate:

- (BOOL) application:(UIApplication *) application 
   didFinishLaunchingWithOptions:(NSDictionary *) launchOptions
{
   UIButton   *button;
   
   [[UIButton appearance] setTintColor:[UIColor orangeColor]];
   button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
   NSLog( @"%@", [button tintColor]);
 }

Step 3: Run and observe console. Notice that it doesn't work, tintColor returns nil. Which is surprising.

The documentation says:

tintColor
The tint color for the button.

@property(nonatomic, retain) UIColor *tintColor
Discussion
The default value is nil.

This property is not valid for all button types.

Wasn't Apple's documentation somewhat better in previous times ? Why not write for what button types tintColor is valid ? Now came the most surprising part. I wrote a little loop to figure out, when tintColor is valid.

   UIButton   *button;
   NSUInteger   i;
   
   for( i = 0; i < 10; i++)
   {
      button = [UIButton buttonWithType:i];
      NSParameterAssert( button);
      [button setTintColor:[UIColor greenColor]];
      NSLog( @"%ld: %@", (long) i, [button tintColor]);
   }

And as it turns out

2013-04-24 15:57:11.496 ApperanceTest[2732:c07] 0: (null)
2013-04-24 15:57:11.498 ApperanceTest[2732:c07] 1: UIDeviceRGBColorSpace 0 1 0 1
2013-04-24 15:57:11.499 ApperanceTest[2732:c07] 2: (null)
2013-04-24 15:57:11.499 ApperanceTest[2732:c07] 3: (null)
2013-04-24 15:57:11.500 ApperanceTest[2732:c07] 4: (null)
2013-04-24 15:57:11.500 ApperanceTest[2732:c07] 5: (null)
2013-04-24 15:57:11.500 ApperanceTest[2732:c07] 6: (null)
2013-04-24 15:57:11.501 ApperanceTest[2732:c07] 7: (null)
2013-04-24 15:57:11.501 ApperanceTest[2732:c07] 8: (null)
2013-04-24 15:57:11.502 ApperanceTest[2732:c07] 9: (null)

the only valid value is ironically 1 which is UIButtonTypeRoundedRect, the value I used in the first place.

So in the end although tintColor is advertised as being modifiable through UIAppearance in the header @property(nonatomic,retain) UIColor *tintColor NS_AVAILABLE_IOS(5_0) UI_APPEARANCE_SELECTOR; // default is nil. only valid for some button types, it really seems not to be the case.

I would on a hunch assume, that's because UIButton could be a class cluster and the subclasses implement their own tintColor accessors and the whole magic breaks down.


Addendum: I should add to my examples, that values from UIApperance only show up in the UIView when the window has been made visible. So the appearance setting should be done ahead of the NIB loading and the reading of values can only be done after [window makeKeyAndVisible]. More obscure magic.

As the button is indeed an instance of a class cluster subclass called UIRoundedRectButton, I tried [[NSClassFromString(@"UIRoundedRectButton") appearance] setTintColor:[UIColor greenColor]]; and then it worked as I'd expected it in the first place.


Another Addendum: The problem is really with UIAppearance instead of UIButton:

Create an empty single view IOS project and create a UIView subclass:

"MulleView.h"

#import <UIKit/UIKit.h>

@interface _MulleView : UIView < UIAppearanceContainer>:
@end

@interface MulleView : _MulleView < UIAppearanceContainer>
@end

and

"MulleView.m"

#import "MulleView.h"


@implementation _MulleView

+ (void) load
{
   [[self appearance] setXColor:[UIColor blueColor]];
}

- (void) setXColor:(UIColor *) color
{
   NSLog( @"%s", __PRETTY_FUNCTION__);
}

@end


@implementation MulleView

- (void) willMoveToWindow:(UIWindow *)newWindow
{
   NSLog( @"%s", __PRETTY_FUNCTION__);
}


- (void) setXColor:(UIColor *) color
{
   NSLog( @"%s", __PRETTY_FUNCTION__);
}

@end

Then set the class of the root view of your xib to MulleView instead of UIView and you will see this in the output when running:

-[MulleView willMoveToWindow:]
-[_MulleView setXColor:]

understandable but wrong for class clusters.