UIButton troubles, and Obj-C magic

When developing SC68 Player for the iPhone I came upon a quite peculiar problem regarding the UIButton.

First the preface

I wanted to add a star button to the right side of each row, subclassing UIButton that can manage selected states, images and all I needed, and use it as the accessory view of the UITableViewCell felt natural. It works and is the right thing to do. The simple factory method is:

Now if this had been all then, simply adding the factory method with a category on UIButton would have been enough. But as it is, a UIButton uses the alpha mask of its’ image as hit target. That hit target is quite small, at least 44 pixels should be used with fingers on a touch screen. Therefor I subclass UIButton so that I can override the hit test as this:

This would allow for an extra 32 pixels to hit on the left and right side of my star button. But now problems arise! Turns out that the overridden implementation of pointInside:withEvent: is never called.

Many hours of troubleshooting later I figured out that the buttonWithType: factory method is the culprit; it do not respect subclasses. Meaning that it will always return instances of UIButton, and thus always call the original implementation of pointInside:withEvent:. Unfortunetely this is the only public factory or init method that can be used to create a button with images only. What to do?

Objective-C to the rescue

With Objective-C we can change the class of an already live object instance, one nice feature of having a dynamic run-time.

All books will tell you that all classes inherits from NSObject. This is not quite true, there is among others NSProxy as well, and you could actually introduce your own root classes if you like. All of them will inherit from the run-time class id. The run-time class id do not implement a single class method, nor instance methods, all it has is a single instance variable: isa of type Class.

As we all should know classes in Objective-C are also object instances, instances of their meta-classes. The isa instance variable is the reference to the object instance’s class instance. And here is the gold nugget; it is write-able. So we can change an object instance’s class by simply assigning a new value to isa.

So lets update the previous factory method to this:

Some precaution

This might look dangerous, and it is. But not so dangerous that you should shun it, the Cocoa frameworks uses it to it’s advantage, and so should you. The only thing you need to be aware of is that the instance size of the original and new classes must be the same. This is easily done by importing runtime.h, and use class_getInstanceSize().

And finally our factory method safely return a new button of the correct subclass, and pointInside:withEvent: is called as expected.

The full source code for SC68 Player where I implemented this is available for download here.

6 Comments

  1. guyal

    Very neat, though how is it that the two classes are the same bytewise size? Are the only size differences between your implementation and Cocoa’s superclass things like having “-32” instead of “0”, and both are treated as a signed int for sizing purposes. As in, depending on how you calculate “size”, the fact that the subclass has a different name and superclass declaration would alone guarantee different sizes…

  2. The instance size of a Objective-C object is always the total size of it’s instance variables, including the instance variables of the super class, and nothing else.

    This means that a subclass can only increase the instance size, never decrease it. And if a subclass do not introduce any instance variables, then it’s instance size is identical to the superclass.

    The class name, class- and instance-methods do not change the instance size; they will change the size of the class. This is not that important to know unless you fiddle with automatic proxy generation, or mock objects. But more info is in the “Objective-C 2.0 Runtime Reference” in the developer documentation.

  3. guyal

    Right. Thanks.

  4. bchamp

    Frank, when I tried this with XCode 3.1.2 and iPhone SDK 2.2.1, it reported that instance variable isa is declared protected and refused to compile. What am I missing?

  5. Ariel

    Frank, I need subclassing UIButton but also I need add some instance variable. Obviusly the sizes of classes are diferent.
    I test it, and aparently run ok.

    It’s OK or maybe I’ll have problems in the future, with more test?

  6. Frank, Great work.

    I’m also subclassing UIButton and adding an instance variable like Ariel.

    Any experience of issues?

    L

Leave a Reply