June 16, 2015

Improving view debugging in Xcode

Normally view debugging in Xcode is somewhat hard: you have to attribute bunch of faceless UIViews to relevant view controller. Here is a before and after:

And the code that made it possible:

#ifdef DEBUG

#import "WMLSwizzler.h"

static SEL wml_loadViewSEL;

static void wml_swizzleLoadViewForClass(Class class) {
    typedef void(*load_view_t)(id, SEL);
    __block load_view_t loadView = (load_view_t)[class S_replaceInstanceMethod:wml_loadViewSEL withBlock:^(UIViewController *self){
        loadView(self, wml_loadViewSEL);
        if (!self.isViewLoaded) {
            return;
        }
        Class viewClass = [self.view class];
        Class viewControllerClass = [self class];
        // in case this method was called twice
        if ([NSStringFromClass(viewClass) hasPrefix:NSStringFromClass(viewControllerClass)]) {
            return;
        }
        NSString *viewClassName = viewClass == [UIView class] ?
            [NSString stringWithFormat:@"%@.view", NSStringFromClass(viewControllerClass)] :
            [NSString stringWithFormat:@"%@.view: %@", NSStringFromClass(viewControllerClass), NSStringFromClass(viewClass)];
        Class newViewClass = NSClassFromString(viewClassName);
        if (!newViewClass) {
            newViewClass = objc_allocateClassPair(viewClass, [viewClassName cStringUsingEncoding:NSUTF8StringEncoding], 0);
            objc_registerClassPair(newViewClass);
        }
        object_setClass(self.view, newViewClass);
    }];
}

static BOOL const wml_isKindOfClass(Class class, Class possibleAncestor) {
    if (class == possibleAncestor) {
        return YES;
    }
    
    do {
        class = class_getSuperclass(class);
        if (class == possibleAncestor) {
            return YES;
        }
    } while (class && class != [NSObject class]);
    return NO;
}

@interface UIViewController (WMLViewDebugging)
@end


@implementation UIViewController (WMLViewDebugging)

+ (void)load {
    @autoreleasepool {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            wml_loadViewSEL = @selector(loadView);
            
            IMP originalIMP = [UIViewController instanceMethodForSelector:wml_loadViewSEL];
            wml_swizzleLoadViewForClass([UIViewController class]);
            
            unsigned int classesCount = 0;
            Class *classList = objc_copyClassList(&classesCount);
            for (unsigned int i = 0; i < classesCount; ++i) {
                Class cls = classList[i];
                const char *cClassName = class_getName(cls);
                if (strcmp(cClassName, "JSExport") == 0 ||
                    strcmp(cClassName, "Object") == 0 ||
                    strcmp(cClassName, "__NSGenericDeallocHandler") == 0 ||
                    strcmp(cClassName, "__NSMessageBuilder") == 0 ||
                    strcmp(cClassName, "_NSZombie_") == 0){
                    continue;
                }
                // If you encounter crash here, add exception to the previous if case
                if (cls == [UIViewController class] ||
                    !wml_isKindOfClass(cls, [UIViewController class]) ||
                    [cls instanceMethodForSelector:wml_loadViewSEL] == originalIMP) {
                    continue;
                }
                wml_swizzleLoadViewForClass(cls);
            }
        });
    }
}

@end

#endif

Obviously this code is guarded to run only in DEBUG.

Although I can think of obscure cases when proposed solution might cause bugs, we’ve been using it in our app for a while and haven’t seen any problems yet. Please do tweet me if you can think of or will run into any issues!