2

I'm trying to parse a JSON file to my iOS app table view. When I launch the app I see that it parses the data, but when I begin to scroll the app instantly crashes and gives me this error: Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[NSNull length]: unrecognized selector sent to instance 0x38094a60'

My code:

#import "FirstViewController.h"
#import "YoutubePost.h"

@interface FirstViewController ()
{
    NSInteger refreshIndex;
    NSArray *title;
    NSArray *about;
    NSArray *views;
    NSArray *rating;
    NSArray *votes;
    NSArray *content;
}
@end

@implementation FirstViewController
@synthesize tweets;

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {
        self.title = NSLocalizedString(@"Videos", @"Videos");
        self.tabBarItem.image = [UIImage imageNamed:@"newtab1"];
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    self.myTableView.separatorColor = [UIColor clearColor];

    [self issueLoadRequest];

    [self setNeedsStatusBarAppearanceUpdate];
}

-(UIStatusBarStyle)preferredStatusBarStyle{
    return UIStatusBarStyleLightContent;
}

#pragma mark - Table view data source

- (void)issueLoadRequest
{
    // Dispatch this block asynchronosly. The block gets JSON data from the specified URL and performs the proper selector when done.
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSData* data = [NSData dataWithContentsOfURL:[NSURL URLWithString:@"my-site.php/file.json"]];
        [self performSelectorOnMainThread:@selector(receiveData:) withObject:data waitUntilDone:YES];
    });
}

- (void)receiveData:(NSData *)data {
    // When we have the data, we serialize it into native cocoa objects. (The outermost element from twitter is
    // going to be an array. I JUST KNOW THIS. Reload the tableview once we have the data.
    self.tweets = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil];
    [self.myTableView reloadData];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return self.tweets.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *simpleTableIdentifier = @"YoutubePost";

    YoutubePost *cell = (YoutubePost *)[tableView dequeueReusableCellWithIdentifier:simpleTableIdentifier];
    if (cell == nil)
    {
        NSArray *nib = [[NSBundle mainBundle] loadNibNamed:@"YoutubePost" owner:self options:nil];
        cell = [nib objectAtIndex:0];
    }

    // The element in the array is going to be a dictionary. I JUST KNOW THIS. The key for the tweet is "text".
    NSDictionary *temp = [self.tweets objectAtIndex:indexPath.row];
    NSDictionary *tweet = [self nullFreeDictionaryWithDictionary:temp];

    cell.title.text = [tweet objectForKey:@"title"];
    cell.views.text = [tweet objectForKey:@"views"];
    return cell;
}

- (NSDictionary *)nullFreeDictionaryWithDictionary:(NSDictionary *)dictionary
{
    NSMutableDictionary *replaced = [NSMutableDictionary dictionaryWithDictionary:dictionary];
    // Iterate through each key-object pair.
    [dictionary enumerateKeysAndObjectsUsingBlock:^(id key, id object, BOOL *stop) {
        // If object is a dictionary, recursively remove NSNull from dictionary.
        if ([object isKindOfClass:[NSDictionary class]]) {
            NSDictionary *innerDict = object;
            replaced[key] = [NSDictionary nullFreeDictionaryWithDictionary:innerDict];
        }
        // If object is an array, enumerate through array.
        else if ([object isKindOfClass:[NSArray class]]) {
            NSMutableArray *nullFreeRecords = [NSMutableArray array];
            for (id record in object) {
                // If object is a dictionary, recursively remove NSNull from dictionary.
                if ([record isKindOfClass:[NSDictionary class]]) {
                    NSDictionary *nullFreeRecord = [NSDictionary nullFreeDictionaryWithDictionary:record];
                    [nullFreeRecords addObject:nullFreeRecord];
                }
                else {
                    if (object == [NSNull null]) {
                        [nullFreeRecords addObject:@""];
                    }
                    else {
                        [nullFreeRecords addObject:record];
                    }
                }
            }
            replaced[key] = nullFreeRecords;
        }
        else {
            // Replace [NSNull null] with nil string "" to avoid having to perform null comparisons while parsing.
            if (object == [NSNull null]) {
                replaced[key] = @"";
            }
        }
    }];

    return [NSDictionary dictionaryWithDictionary:replaced];
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    return 397;
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{

    int storyIndex = [indexPath indexAtPosition: [indexPath length] - 1];

    NSString * storyLink = [[tweets objectAtIndex: storyIndex] objectForKey:@"link"];

    [[UIApplication sharedApplication] openURL:[NSURL URLWithString:storyLink]];
    // Spit out some pretty JSON for the tweet that was tapped. Neato.
    NSString *formattedJSON = [[NSString alloc] initWithData:[NSJSONSerialization dataWithJSONObject:[self.tweets objectAtIndex:indexPath.row] options:NSJSONWritingPrettyPrinted error:nil] encoding:NSUTF8StringEncoding];
    NSLog(@"tweet:\n%@", formattedJSON);
}


@end

Before I installed the new Xcode 5 I didn't get this error. Can someone help me?

Thanks.

tracifycray
  • 1,423
  • 4
  • 18
  • 29
  • 2
    NSNull does not support the "length" method. And you have an NSNull in your data, representing a JSON element that was transmitted as "null". (If you look, the message gives you a line number to tell you precisely what reference hit the problem. Then you simply need to add the logic to detect NSNull and handle that case.) – Hot Licks Oct 28 '13 at 19:48
  • I'm sorry, I'm just a beginner. What exactly should I do? @HotLicks – tracifycray Oct 28 '13 at 20:17
  • You can check that the object is NSNull by checking [object isKindOfClass [NSNull class]]. – slecorne Oct 28 '13 at 21:10
  • At the very least, copy/paste the FULL exception message (including line number) into your question, and then make sure that the code you've listed includes that line number. This is most basic stuff necessary for debugging. – Hot Licks Oct 28 '13 at 21:10
  • for me, these DO NOT SHOW the line number ... it sucks! – Fattie Jul 31 '14 at 14:28

1 Answers1

8

What I've been doing myself to avoid this when parsing JSON results is replacing each instance of NSNull with a null string (@""), using the following method:

+ (NSDictionary *)nullFreeDictionaryWithDictionary:(NSDictionary *)dictionary
{
    NSMutableDictionary *replaced = [NSMutableDictionary dictionaryWithDictionary:dictionary];
    // Iterate through each key-object pair.
    [dictionary enumerateKeysAndObjectsUsingBlock:^(id key, id object, BOOL *stop) {
        // If object is a dictionary, recursively remove NSNull from dictionary.
        if ([object isKindOfClass:[NSDictionary class]]) {
            NSDictionary *innerDict = object;
            replaced[key] = [NSDictionary nullFreeDictionaryWithDictionary:innerDict];
        }
        // If object is an array, enumerate through array.
        else if ([object isKindOfClass:[NSArray class]]) {
            NSMutableArray *nullFreeRecords = [NSMutableArray array];
            for (id record in object) {
                // If object is a dictionary, recursively remove NSNull from dictionary.
                if ([record isKindOfClass:[NSDictionary class]]) {
                    NSDictionary *nullFreeRecord = [NSDictionary nullFreeDictionaryWithDictionary:record];
                    [nullFreeRecords addObject:nullFreeRecord];
                }
                else {
                    if (object == [NSNull null]) {
                        [nullFreeRecords addObject:@""];
                    }
                    else {
                        [nullFreeRecords addObject:record];
                    }
                }
            }
            replaced[key] = nullFreeRecords;
        }
        else {
            // Replace [NSNull null] with nil string "" to avoid having to perform null comparisons while parsing.
            if (object == [NSNull null]) {
                replaced[key] = @"";
            }
        }
    }];

    return [NSDictionary dictionaryWithDictionary:replaced];
}

Of course, this relies on the JSON return format being a dictionary, but you could easily modify it to accommodate other data types if you replaced all the parameter types with id and performed class checks.

--Edit-- If this is the only place you'll be using JSON, then change

NSDictionary *tweet = [self.tweets objectAtIndex:indexPath.row]; 

to

NSDictionary *temp = [self.tweets objectAtIndex:indexPath.row];
NSDictionary *tweet = [NSDictionary nullFreeDictionaryWithDictionary:temp];

Note that I have nullFreeDictionaryWithDictionary as an Objective-C category for the NSDictionary class. You could probably just add that to your view controller's implementation file if you weren't going to use this method anywhere else.

Kamaros
  • 4,536
  • 1
  • 24
  • 39
  • Ok, great. Thanks. I'm a beginner, so how could I implement this? @Kamaros – tracifycray Oct 29 '13 at 07:57
  • So should I create this: NSDictionary *nullFreeDictionaryWithDictionary; in the header and then synthesize is it main? It won't work, it just says: No known class method for selector 'nullFreeDictionaryWithDictionary:' @Kamaros – tracifycray Oct 29 '13 at 15:15
  • That's because I wrote it as a category, to make it more reusable. In your case, change the minus sign to a plus sign, and call `NSDictionary *tweet = [self nullFreeDictionaryWithDictionary:temp];` instead of `NSDictionary *tweet = [NSDictionary nullFreeDictionaryWithDictionary:temp];` – Kamaros Oct 29 '13 at 16:23
  • I've changed the minus sign to a plus sign, but it gives me: No known class method for selector 'nullFreeDictionaryWithDictionary:... @Kamaros – tracifycray Oct 29 '13 at 19:16
  • Er, I mean change the plus sign to a minus sign. My bad. – Kamaros Oct 29 '13 at 19:34
  • Did you replace all the `[NSDictionary nullFreeDictionaryWithDictionary]` calls within the method itself (it's recursive) to [`self nullFreeDictionaryWithDictionary]` as well? – Kamaros Oct 29 '13 at 19:39
  • Yup, I did that @Kamaros – tracifycray Oct 29 '13 at 19:42
  • If you take a careful look at the implementation for `nullFreeDictionaryWithDictionary`, you'll see two lines where you're calling `[NSDictionary nullFreeDictionaryWithDictionary:innerDict]` and `[NSDictionary nullFreeDictionaryWithDictionary:record]`. You need to replace `NSDictionary` with `self` in those two locations. – Kamaros Oct 29 '13 at 20:05
  • A cleaner solution would be adding a category to NSDictionary and import this category in your prefix header, so you can use it everywhere in your app. The category should implement functionality to regard every `[NSNull null]` value as a `nil` value instead. It's safe to call methods on nil objects in Objective-C. See: http://stackoverflow.com/questions/5716942/touchjson-dealing-with-nsnull/5719428#5719428 – Wolfgang Schreurs Oct 29 '13 at 20:10
  • You'd probably want two methods, because you need to decide whether a null in your incoming data is more as if the key isn't present (should return nil) or whether it is more like an empty string (should return @""). – gnasher729 Apr 04 '14 at 16:41
  • Can I just say that I love you. – chandhooguy Jan 08 '15 at 03:17