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
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.