Search code examples
objective-ccocoansarray

How to customise description of NSArray?


I would like to provide my own implementation for description method of NSArray class, so that I could use it simply like this:

NSLog(@"%@", @[]);

My idea was to provide a category for NSArray and simply override description method there. However it doesn't work because NSArray is a class cluster and its real class is __NSArrayI, so my category implementation is never called. Unfortunately I cannot provide a category for __NSArrayI because this class is unavailable.

Of course, I can just subclass NSArray and implement this method in my subclass, but again, since NSArray is a class cluster I have to provide an implementation for a bunch of different methods, like objectAtIndex: and I don't wanna do that because this is too much work for simply changing the way an array is printed into console.

Do you have any ideas, guys? Thanks


Solution

  • Do you have any ideas, guys? Thanks

    Ideas we got. Solutions... you gotta decide.

    From the documentation about format specifiers, you can't just worry about description. Specifically, from that document...

    Objective-C object, printed as the string returned by descriptionWithLocale: if available, or description otherwise. Also works with CFTypeRef objects, returning the result of the CFCopyDescription function.

    We can't do much about CFTypeRef objects unless we want to hack with the linker and/or dynamic loader.

    However, we can do something about description and descriptionWithLocale:, though that something is a bit gnarly.

    You may also want to consider debugDescription as well.

    This is one way to approach your goal, though I consider it "educational" and you should use your best judgement as to whether you want to go this route or not.

    First, you need to determine what your replacement description implementation would look like. We will declare a simplistic replacement for description like so (ignoring the original implementation).

    static NSString * swizzledDescription(id self, SEL _cmd)
    {
        NSUInteger count = [self count];
        NSMutableString *result = [NSMutableString stringWithFormat:@"Array instance (%p) of type %@ with %lu elements", (void*)self, NSStringFromClass([self class]), (unsigned long)count];
        int fmtLen = snprintf(0,0,"%lu",count);
        for (NSUInteger i = 0; i < count; ++i) {
            [result appendFormat:@"\n%p: %*lu: %@", (void*)self, fmtLen, i, self[i]];
        }
        return result;
    }
    

    and an even more simplistic implementation of descriptionWithLocale: that completely ignores the locale.

    static NSString * swizzledDescriptionWithLocale(id self, SEL _cmd, id locale) {
        return swizzledDescription(self, _cmd);
    }
    

    Now, how do we make the NSArray implementations use this code? One approach is to find all subclasses of NSArray and replace their methods...

    static void swizzleMethod(Class class, SEL selector, IMP newImp) {
        Method method = class_getInstanceMethod(class, selector);
        if (method) {
            IMP origImp = method_getImplementation(method);
            if (origImp != newImp) {
                method_setImplementation(method, newImp);
            }
        }
    }
    
    static void swizzleArrayDescriptions() {
        int numClasses = objc_getClassList(NULL, 0);
        if (numClasses <= 0) return;
        Class *classes = (__unsafe_unretained Class *)malloc(sizeof(Class) * numClasses);
        numClasses = objc_getClassList(classes, numClasses);
    
        Class target = [NSArray class];
        for (int i = 0; i < numClasses; i++) {
            for (Class c = classes[i]; c; c = class_getSuperclass(c)) {
                if (c == target) {
                    c = classes[i];
                    swizzleMethod(c, @selector(description), (IMP)swizzledDescription);
                    swizzleMethod(c, @selector(descriptionWithLocale:), (IMP)swizzledDescriptionWithLocale);
                    break;
                }
            }
        }
        free(classes);
    }
    

    A reasonable place to call swizzleArrayDescriptions is in your app delegate's +initialize method.

    @implementation AppDelegate
    
    + (void)initialize {
        if (self == [AppDelegate class]) {
            swizzleArrayDescriptions();
        }
    }
    

    Now, you should be able to play with it and see how you get along.

    As a very simple test...

    - (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
        // Insert code here to initialize your application
        NSArray *array = @[@"One", @"Two", @3, @"4", @"FIVE", @(6.0), @".7.", @8, @9, @10, @"Eleven" ];
        NSLog(@"%@", array);
        NSLog(@"%@", [array mutableCopy]);
    }
    

    yields this output...

    2015-11-08 14:30:45.501 TestApp[72183:25861219] Array instance (0x6000000c5780) of type __NSArrayI with 11 elements
    0x6000000c5780:  0: One
    0x6000000c5780:  1: Two
    0x6000000c5780:  2: 3
    0x6000000c5780:  3: 4
    0x6000000c5780:  4: FIVE
    0x6000000c5780:  5: 6
    0x6000000c5780:  6: .7.
    0x6000000c5780:  7: 8
    0x6000000c5780:  8: 9
    0x6000000c5780:  9: 10
    0x6000000c5780: 10: Eleven
    2015-11-08 14:30:45.501 TestApp[72183:25861219] Array instance (0x600000045580) of type __NSArrayM with 11 elements
    0x600000045580:  0: One
    0x600000045580:  1: Two
    0x600000045580:  2: 3
    0x600000045580:  3: 4
    0x600000045580:  4: FIVE
    0x600000045580:  5: 6
    0x600000045580:  6: .7.
    0x600000045580:  7: 8
    0x600000045580:  8: 9
    0x600000045580:  9: 10
    0x600000045580: 10: Eleven
    

    Of course, you should do more testing than I did, because all I did was hack this up after church because it seemed somewhat interesting (it's raining so the picnic was cancelled).

    If you need to invoke the original implementation, you will need to create a cache of the original implementations, keyed by class, and invoke them accordingly. However, for a case like this where you want to modify the returned string, you probably don't need to do that, and it should be straight forward in any event.

    Also, please note the normal cautions about swizzling, and they are a bit more heightened when playing with class clusters.

    Note, that you could also do something like this to create custom subclasses at runtime as well. You could even define your subclasses just as straight subclasses of NSArray in the normal way, then swizzle their types without impacting any of the classes that are not yours... or a bunch of different things... remember the ObjectiveC runtime is your friend.