Saturday, 23 April 2016

Superclass with a Category Factory method

tl;dr If you want to make a factory method in Objective-C using a category and still get it to work for subclasses, you can do something like this:

@implementation UIButton (ColorButton)
+(instancetype)buttonWithColor:(UIColor*)color {
    UIButton* button = [[self.class alloc] initWithFrame:CGRectZero];
    ...
    return button;
}


The Problem

This situation came up recently when I had a category class method that made a button with a certain background colour, but I wanted to customise the button to change background colour when it was highlighted or in the touch down state.

The category looked like this:

@implementation UIButton (ColorButton)
+(UIButton)buttonWithColor:(UIColor*)color {
    UIButton* button = [[UIButton allocinitWithFrame:CGRectMake(0,0,UIScreen.mainScreen.bounds.size.width,50)];
    button.backgroundColor = color;
    return button;
}
@end

This method takes a UIColor and returns a UIButton with a size and colour.

In the subclass, I needed to override the setHighlighted: method:


@implementation ColorButton
- (void) setHighlighted:(BOOL)highlighted {
    [super setHighlighted:highlighted];
    if (highlighted) {
        self.backgroundColor = self.colour_selected;
    }
    else {
        self.backgroundColor = self.colour;
    }
}
@end

This allows me to pick two colours colour_selected and colour to be the tap down and tap up colours respectively. I tried using my new subclass with:

ColorButton* button_new = [ColorButton buttonWithColor:[UIColor whiteColor]];
button_new.colour = [UIColor whiteColor];
button_new.colour_selected = [UIColor grayColor];

However when I tried to compile this code and I got the following warning:


incompatible pointer types assigning to 'ColorButton *' from 'UIButton *'


The first fix

Okay. I tried changing +(UIButton)buttonWithColor:(UIColor*)color to
+(instancetype)buttonWithColor:(UIColor*)color. Now the code compiled and ran, but it crashed when it tried to set the colour property on button_new.


The second fix

It crashed because button_new was actually a UIButton and not a ColorButton. To fix this I needed to look at the code in the buttonWithColor: method a little closer.

@implementation UIButton (ColorButton)
+(instancetype)buttonWithColor:(UIColor*)color {
    UIButton* button = [[UIButton allocinitWithFrame:CGRectMake(0,0,UIScreen.mainScreen.bounds.size.width,50)];
    button.backgroundColor = color;
    return button;
}
@end

Notice the call to alloc is for a UIButton. How did I make it work for the current class just like I did with instancetype? I replaced the UIButton with self.class:

@implementation UIButton (ColorButton)
+(instancetype)buttonWithColor:(UIColor*)color {
    UIButton* button = [[self.class allocinitWithFrame:CGRectMake(0,0,UIScreen.mainScreen.bounds.size.width,50)];
    button.backgroundColor = color;
    return button;
}
@end

And that's it. The code runs and the button changes colour when it's tapped. But what is self in this case and what does self.class do? I guess that question could be a topic for another blog post.

No comments:

Post a Comment