Dynamic Subclassing

If you’ve seen my github.com page or my portfolio link, you’re probably aware of a project of mine called CHLayoutManager. CHLayoutManager, for those of you not in-the-know, is a way to define the layout of a user interface on the Mac via constraints (as opposed to autoresizing masks). For example, if you have two buttons “A” and “B”, you can say “I want the right edge of button ‘A’ to always stay 10 points to the left of the left edge of button ‘B’”. Then, as button “B” moves around (via autoresizing masks or positioning it programmatically), button “A” automatically moves as well. If you do a lot of programmatic UI layouting, this can be insanely useful. In fact, a couple forthcoming products from Mozy will be using CHLayoutManager in them.

Internally, there’s a singleton object called the CHLayoutManager. This object is the one responsible for noticing when a view changes its frame, and then also determining if any other views need to change because of it. This layout manager also has an NSMapTable for storing all of the constraint information related to views. The key of the map table is the view itself, and the value is a container for storing the constraints and the layout name. Because this is supposed to operate silently in the background, the map table maintains a weak reference on the view, and a strong reference to the constraint container. That means if you’re using garbage collection and a view is deallocated, the garbage collector will automatically clean up the entry in the map table, and all will be well in the world.

However, if you’re not using garbage collection, some interesting behavior can crop up. For example, let’s say the map table contains a number of key-value pairs, and some of the keys point to views that have been deallocated (ie, the pointers are invalid). At some point in the future, when you attempt to add a constraint to a new view, the map table will need to resize itself. This means that it will reorganize its contents. Part of this step involves invoking -hash on each key.

But wait. Some of the keys are invalid pointers. If you try to send a message to a deallocated object, you’re (almost definitely) going to crash. Herein lies the crux of the problem: how can we ensure that each view automatically gets cleaned up from the map table without requiring the programmer to do it manually, and without relying on garbage collection?

The answer: dynamic subclassing.

When the user adds a constraint to a view, the layout manager is going to introspect this view and see if the view needs to be altered. If it does, then the manager is going to create a new class that’s a subclass of the view’s class. To this new class gets added a new method: a custom -dealloc method. This method performs the cleanup of the view’s constraints (and other layout information), then invokes [super dealloc]. Once we’ve created this subclass, we simply change the class of the view, and we’re good to go.

What does this look like? Like this:

- (void) dynamicallySubclassView:(NSView *)view {
  const char * prefix = "CHLayoutAutoremove_";
  Class viewClass = [view class];
  NSString * className = NSStringFromClass(viewClass);
  if (strncmp(prefix, [className UTF8String], strlen(prefix)) == 0) { return; }

  NSString * subclassName = [NSString stringWithFormat:@"%s%@", prefix, className];
  Class subclass = NSClassFromString(subclassName);

  if (subclass == nil) {
    subclass = objc_allocateClassPair(viewClass, [subclassName UTF8String], 0);
    if (subclass != nil) {
      IMP dealloc = class_getMethodImplementation([self class], @selector(dynamicDealloc));

      class_addMethod(subclass, @selector(dealloc), dealloc, "v@:");
      objc_registerClassPair(subclass);
    }
  }

  if (subclass != nil) {
    object_setClass(view, subclass);
  }
}

Here you can see what’s going on:

  1. Extract the view’s class
  2. See if the name of this class begins with the prefix used to indicate one of these dynamic subclasses
  3. If it doesn’t have the prefix, then build the name of the new class (+[NSString stringWithFormat:])
  4. Look in the runtime to see if a class of this name already exists
  5. If it doesn’t exist, create the class using objc_allocateClassPair()
  6. Add the custom -dealloc method to the new subclass
  7. Register the class with the runtime
  8. If everything went well, set the class of the view to the new subclass

So if you have an NSPopUpButton and add some constraints to it, it’s actually going to be an CHLayoutAutoremove_NSPopUpButton.

This is, incidentally, how Key-Value Observing is implemented in Cocoa and Cocoa Touch.

Isn’t Objective-C fun?

Drawing a gradient-filled rounded rect with a drop shadow

This is something I’ve been fiddling with for the past few hours trying to get to work right. I tried doing stuff with CoreGraphics, drawing paths, drawing images, and so on, but it never quite worked correctly. Then I stumbled upon this post over at cimgf.com, which helped me figure it out.

In a custom UIView subclass, I have this:

- (void) awakeFromNib {
  //turning off bounds clipping allows the shadow to extend beyond the rect of the view
  [self setClipsToBounds:NO];

  //the colors for the gradient.  highColor is at the top, lowColor as at the bottom
  UIColor * highColor = [UIColor colorWithWhite:1.000 alpha:1.000];
  UIColor * lowColor = [UIColor colorWithRed:0.851 green:0.859 blue:0.867 alpha:1.000];

  //The gradient, simply enough.  It is a rectangle
  CAGradientLayer * gradient = [CAGradientLayer layer];
  [gradient setFrame:[self bounds]];
  [gradient setColors:[NSArray arrayWithObjects:(id)[highColor CGColor], (id)[lowColor CGColor], nil]];

  //the rounded rect, with a corner radius of 6 points.
  //this *does* maskToBounds so that any sublayers are masked
  //this allows the gradient to appear to have rounded corners
  CALayer * roundRect = [CALayer layer];
  [roundRect setFrame:[self bounds]];
  [roundRect setCornerRadius:6.0f];
  [roundRect setMasksToBounds:YES];
  [roundRect addSublayer:gradient];

  //add the rounded rect layer underneath all other layers of the view
  [[self layer] insertSublayer:roundRect atIndex:0];

  //set the shadow on the view's layer
  [[self layer] setShadowColor:[[UIColor blackColor] CGColor]];
  [[self layer] setShadowOffset:CGSizeMake(0, 6)];
  [[self layer] setShadowOpacity:1.0];
  [[self layer] setShadowRadius:10.0];
}

This works perfectly. :)

Aspect Oriented Programming

Recently in a CS class of mine, we briefly discussed Aspect Oriented Programming. The idea intrigued me, and knowing some of the capabilities of the Objective-C runtime, I decided to play around with it and see what I could do.

I first needed a good aspect that I could work on. After a bit of thought, I decided that NSCoding was a good candidate. Usually, Cocoa developers implement NSCoding in order to serialize a custom object. My preferred way is via NSKeyedArchiver and NSKeyedUnarchiver. I realized that whenever I implement the requisite initWithCoder: and encodeWithCoder: methods, they’re almost always the same thing: Save all the instance variables.

So what I’ve come up with is what I call “NSCodingAspect”. The idea of the class is simple: It has one public class method called addToClass:error:, and that method will add NSCoding compliance to whatever class is passed in.

Included below is the full .m file for NSCodingAspect. This is my first stab at it, and it’s probably not the cleanest, but I think it’s pretty darn nifty:

#import "NSCodingAspect.h"
#import <objc/runtime.h>

@implementation NSCodingAspect

+ (void) addMethod:(SEL)aSelector toClass:(Class)aClass error:(NSError **)error {
  IMP implementation = class_getMethodImplementation([self class], aSelector);
  Method method = class_getInstanceMethod([self class], aSelector);
  NSLog(@"  Adding -[%@ %@]", NSStringFromClass(aClass), NSStringFromSelector(aSelector));
  BOOL worked = class_addMethod(aClass, aSelector, implementation, method_getTypeEncoding(method));
  if (!worked) {
    *error = [NSError errorWithDomain:NSStringFromClass(aClass) 
                             code:0 
                         userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Error adding method: %@", 
                                                                     NSStringFromSelector(aSelector)] 
                                                              forKey:@"errMsg"]];
  } else {
    error = nil;
  }
}

+ (void) addToClass:(Class)aClass error:(NSError **)error {
  Protocol * codingProtocol = objc_getProtocol("NSCoding");
  BOOL classConforms = class_conformsToProtocol(aClass, codingProtocol);
  NSString * className = NSStringFromClass(aClass);
  NSLog(@"Conforming [%@ class] to <NSCoding>", className);

  if (!classConforms) {
    class_addProtocol(aClass, codingProtocol);

    if (!class_getInstanceMethod(aClass, @selector(initWithCoder:))) {
      [NSCodingAspect addMethod:@selector(initWithCoder:) toClass:aClass error:error];
      if (error) { return; }
    }
    if (!class_getInstanceMethod(aClass, @selector(encodeWithCoder:))) {
      [NSCodingAspect addMethod:@selector(encodeWithCoder:) toClass:aClass error:error];
      if (error) { return; }
    }
    //all the ivars need to conform to NSCoding, too
    unsigned int numIvars = 0;
    Ivar * ivars = class_copyIvarList(aClass, &numIvars);
    for(int i = 0; i < numIvars; i++) {
      NSString * type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivars[i])];
      if ([type length] > 3) {
        NSString * class = [type substringWithRange:NSMakeRange(2, [type length]-3)];
        Class ivarClass = NSClassFromString(class);
        [NSCodingAspect addToClass:ivarClass error:error];
      }
    }
  }
}

- (id) initWithCoder:(NSCoder *)decoder {
  if ([super respondsToSelector:@selector(initWithCoder:)] && ![self isKindOfClass:[super class]]) {
    self = [super performSelector:@selector(initWithCoder:) withObject:decoder];
  } else {
    self = [super init];
  }
  if (self == nil) { return nil; }

  NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
  unsigned int numIvars = 0;
  Ivar * ivars = class_copyIvarList([self class], &numIvars);
  for(int i = 0; i < numIvars; i++) {
    Ivar thisIvar = ivars[i];
    NSString * key = [NSString stringWithUTF8String:ivar_getName(thisIvar)];
    id value = [decoder decodeObjectForKey:key];
    if (value == nil) { value = [NSNumber numberWithFloat:0.0]; }
    [self setValue:value forKey:key];
  }
  if (numIvars > 0) { free(ivars); }
  [pool drain];
  return self;
}

- (void) encodeWithCoder:(NSCoder *)encoder {
  if ([super respondsToSelector:@selector(encodeWithCoder:)] && ![self isKindOfClass:[super class]]) {
    [super performSelector:@selector(encodeWithCoder:) withObject:encoder];
  }
  NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
  unsigned int numIvars = 0;
  Ivar * ivars = class_copyIvarList([self class], &numIvars);
  for (int i = 0; i < numIvars; i++) {
    Ivar thisIvar = ivars[i];
    NSString * key = [NSString stringWithUTF8String:ivar_getName(thisIvar)];
    id value = [self valueForKey:key];
    [encoder encodeObject:value forKey:key];
  }
  if (numIvars > 0) { free(ivars); }
  [pool drain];
}

@end

Here’s the general idea of what it’s doing:

  1. It checks to see if the class already conforms to NSCoding. If it does, then it exits.
  2. If not, it proceeds to copy the two NSCoding methods from NSCodingAspect into the target class.
  3. It then loops through all the instance variables of the target class to make sure that they all conform to NSCoding as well. If any don’t, NSCoding gets added to them, too

The implementation of the initWithCoder: and encodeWithCoder: methods are as follows:

  1. Check to see if the superclass needs to init or encode. If so, then go do that.
  2. Get the list of all the instance variables
  3. For each instance variable, get its name, convert it to an NSString, and then either encode or decode the value that corresponds to that instance variable

In reality, the logic is pretty straightforward. What’s so neat about this is that it’s even possible! You can’t do this with most compiled languages, since the information on what methods or instance variables a class has are statically compiled into the code.

It’s stuff like this that make me absolutely LOVE working in Objective-C.

(While not tested, the above code should work just fine on the iPhone)