Question

This has been bugging me for a while. How do I counteract the ugly escaping that happens when dumping objects in the debugger with po foo (or via NSLog). I've tried numerous approaches to implementing -description or -debugDescription to no avail.

Given this simple class

@interface Foo : NSObject
@property NSDictionary* dict;
@end

@implementation Foo
- (NSString *)description {
    // super.description for the <{classname} pointer> output
    return [NSString stringWithFormat:@"%@ %@", super.description, self.dict];
}
@end

And contrived usage

Foo* f0 = [[Foo alloc] init];
f0.dict = @{ @"value": @0, @"next": NSNull.null };
Foo* f1 = [[Foo alloc] init];
f1.dict = @{ @"value": @1, @"next": f0 };
Foo* f2 = [[Foo alloc] init];
f2.dict = @{ @"value": @2, @"next": f1 };

We get nice output for f0

(lldb) po f0
<Foo: 0x8cbc410> {
    next = "<null>";
    value = 0;
}

Tolerable output for f1

(lldb) po f1
<Foo: 0x8cbc480> {
    next = "<Foo: 0x8cbc410> {\n    next = \"<null>\";\n    value = 0;\n}";
    value = 1;
}

And horrendous output for f2

(lldb) po f2
<Foo: 0x8cbc4b0> {
    next = "<Foo: 0x8cbc480> {\n    next = \"<Foo: 0x8cbc410> {\\n    next = \\\"<null>\\\";\\n    value = 0;\\n}\";\n    value = 1;\n}";
    value = 2;
}

This gets hard to parse fast when debugging real world object hierarchies. I'm assuming there's some other trick I'm missing since dumping a similarly nested NSDictionary

NSDictionary* d0 = @{ @"value": @0, @"next": NSNull.null };
NSDictionary* d1 = @{ @"value": @1, @"next": d0 };
NSDictionary* d2 = @{ @"value": @2, @"next": d1 };

Maintains the indenting and avoids the escaping hell

(lldb) po d2
{
    next =     {
        next =         {
            next = "<null>";
            value = 0;
        };
        value = 1;
    };
    value = 2;
}

UPDATE

Switching to -debugDescription and simply forwarding to the dictionary

@implementation Foo
- (NSString *)debugDescription {
    return self.dict.debugDescription;
}
@end

loses the recursive output

(lldb) po f2
{
    next = "<Foo: 0x8b70e20>";
    value = 2;
}

Internally NSDictionary must be relying on -description which I'm not implementing in this example, only -debugDescription. Switching to something like the following

@implementation Foo
- (NSString *)description {
    return self.dict.description;
}
- (NSString *)debugDescription {
    return self.dict.debugDescription;
}
@end

produces similarly bad output as well

(lldb) po f2
{
    next = "{\n    next = \"{\\n    next = \\\"<null>\\\";\\n    value = 0;\\n}\";\n    value = 1;\n}";
    value = 2;
}
Was it helpful?

Solution

TL;DR;

Use NSContainers-PrettyPrint and carefully read the docs.

Long Answer

After much more searching I discovered the descriptionWithLocale:indent: method. As documented, I should've been able to implement this in my own classes to achieve the desired pretty-print formatting. However, after some failed attempts I found a similar SO question. Turns out descriptionWithLocale:indent: only works if you subclass a Foundation container class because of "security concerns".

Unsatisfied with that approach I continued digging and found this radar but also a solution in NSContainers-PrettyPrint. After some trial and error I got things working decently well. (It's not on CocoaPods so you have to add it manually).

Once NSContainers-PrettyPrint is added you'll probably want JRSwizzle too. Then define DEBUGPRINT_ALL and DEBUGPRINT_SWIZZLE in your DEBUG targets preprocessor macros. Finally, you can implement your descriptionWithLocale:indent: methods in terms of the fs_* helpers and best practices.

Using the same Foo from my question as an example

@implementation Foo
- (NSString*)description
{
    return [NSString stringWithFormat:@"%@ %@", super.description, self.dict.description];
}

- (NSString *)descriptionWithLocale:(id)locale indent:(NSUInteger)level
{
    NSString * indent = [NSString fs_stringByFillingWithCharacter:' ' repeated:fspp_spacesPerIndent*level];
    NSMutableString * str = [[NSMutableString alloc] init];
    [str fs_appendObjectStartWithIndentString:indent caller:self];
    [str appendString:[self.dict descriptionWithLocale:locale indent:level+1]];
    [str fs_appendObjectEnd];
    return str;
}
@end

Would produce the following output given the same f0, f1 and f2 instances

(lldb) po f0
<Foo: 0x8a385c0> {
    value = 0;
    next = <null>;
}
(lldb) po f1
<Foo: 0x8a38630> {
    value = 1;
    next = <Foo:0x8a385c0        {
            value = 0;
            next = <null>;
        }>;
}
(lldb) po f2
<Foo: 0x8a38660> {
    value = 2;
    next = <Foo:0x8a38630        {
            value = 1;
            next = <Foo:0x8a385c0                {
                    value = 0;
                    next = <null>;
                }>;
        }>;
}

The above descriptionWithLocale:indent: could use some tweaking to reduce the excessive whitespace but it still beats the alternatives.

OTHER TIPS

A similar question (NSDictionary description formatting problem — treats structure like char data) suggested that NSArray and NSDictionary are in some way cheating and not calling -[NSObject respondsToSelector:@selector(descriptionWithLocale:indent:). This appears to be true.

To test this I created an NSProxy which log all calls. The test was:

NSObject *proxy = [LoggingProxy proxyWithTarget:[[NSObject alloc] init]];
NSLog(@"%@", @[proxy]);

The results were:

message: isNSString__
message: isNSDictionary__
message: isNSArray__
message: isNSData__

2014-05-29 15:29:22.728 Proxy[36219:303] (
    "<LoggingProxy: 0x100103510>"
)
Program ended with exit code: 0

In my real objects I added the method:

#if DEBUG
- (BOOL) isNSDictionary__
{
    return YES;
}
#endif

NSArray and NSDictionary then behaved as you'd hope by calling -[NSObject respondsToSelector:@selector(descriptionWithLocale:indent:). Make sure that if you return YES you really have implemented descriptionWithLocale:indent: as it will be called with out checking it's really there.

Please remember to wrap this method in #if DEBUG. You really don't want to be shipping this, but it seems fine for use in Xcode.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top