3
\$\begingroup\$

Was advised to post this question here after posting to Stack Overflow. I'm pre-populating a UITableView app with data using a local JSON file. The JSON itself is very simple:

[
    {"Name":"Tennis Ball Launchers",
    "Type":"Forces"},

    {"Name":"Magnetic Hot Plates",
    "Type":"Magnetism"},

    {"Name":"Giant Lever",
    "Type":"Forces"},

    {"Name":"Music Wall",
    "Type":"Sound"},

    {"Name":"Water Bearing",
    "Type":"Forces"},

    {"Name":"Chaos Pendulums",
    "Type":"Chaos"}
]

The JSON objects will have more properties further down the line, this is just for starters (there will be around 200 entries by the end, with more properties). I don't imagine the structure getting any more complex, however. The user will not be adding or deleting entries in the app - the JSON is read only. I want to map these JSON objects into an array of custom NSObjects. Here is the code I am currently using in the initialisation of my data model:

    // JSON reading into array
    NSString *filePath = [[NSBundle mainBundle] pathForResource:@"test" ofType:@"json"];

    NSError *error;

    NSString *fileContents = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:&error];

    if(error) {
        NSLog(@"Error reading file: %@", error.localizedDescription);
    }

    // Get JSON objects into initial array
    NSArray *rawExhibits = (NSArray *)[NSJSONSerialization JSONObjectWithData:[fileContents dataUsingEncoding:NSUTF8StringEncoding] options:0 error:NULL];

    NSMutableArray *myItems = [[NSMutableArray alloc] init];

    for (NSDictionary *object in rawExhibits) {
        NSString *objectName = object[@"Name"];
        NSString *objectType = object[@"Type"];

        // Create new exhibit object with data procured from JSON object
        W5BExhibit *exhibit = [[W5BExhibit alloc] initWithExhibitName:objectName exhibitType:objectType];

        [myItems addObject:exhibit];
    }

    self.allExhibits = [[NSArray alloc] initWithArray:myItems];

The table view can then refer to the allExhibits variable which is a property of a singleton. I'm very new at this so I'm canvassing for opinions.

I was also advised to consider using keyed archiving to persist the data instead of parsing the JSON each time the app is opened up from new.

\$\endgroup\$
  • 1
    \$\begingroup\$ I would point out that, unless your objects define a lot of special methods, there's no great advantage to placing the data into "custom" objects vs leaving the NSDictionarys as they are. \$\endgroup\$ – Hot Licks Oct 10 '14 at 23:20
  • \$\begingroup\$ Fair point. I thought about that as I was writing this but I wanted to follow an example from the book I'm using. \$\endgroup\$ – bunglenutter Oct 11 '14 at 10:14
9
\$\begingroup\$

The correct way to check for success or failure of Cocoa (Touch) methods is documented in "Handling Error Objects Returned From Methods" in the "Error Handling Programming Guide":

Important: Success or failure is indicated by the return value of the method. Although Cocoa methods that indirectly return error objects in the Cocoa error domain are guaranteed to return such objects if the method indicates failure by directly returning nil or NO, you should always check that the return value is nil or NO before attempting to do anything with the NSError object.

Which means that the result of

NSString *fileContents = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:&error];

should be checked with

if (fileContents == nil) { // Not: if (error) { ...
    NSLog(@"Error reading file: %@", error.localizedDescription);
}

And you should also use the error parameter and check the result of

NSArray *rawExhibits = (NSArray *)[NSJSONSerialization JSONObjectWithData:[fileContents dataUsingEncoding:NSUTF8StringEncoding] options:0 error:NULL];

The explicit cast to (NSArray *) is not necessary because the method returns id and that can be assigned to any Objective-C object pointer.

Actually your method to read the JSON data is too complicated: The file contents is first read into a string (using some encoding) and then converted to back to data (using the same encoding). This can be simplified by reading the file into data in the first step:

NSData *jsonData = [NSData dataWithContentsOfFile:filePath];

You also have to decide what to do if reading the JSON data fails. Just logging the error is not sufficient because then the program will crash later.


The parameter naming in your init method

-initWithExhibitName:exhibitType:

seems a bit too verbose to me. It is clear from the class name W5BExhibit that the object is some "exhibit", so I would shorten that too

-initWithName:type:

Adding new properties to your object class and the JSON file currently requires a new init method with more arguments. This can be greatly simplified using "Key-Value Coding" if you use the exact property names of your custom class as keys in the dictionary.

So assuming that W5BExhibit is defined as

@interface W5BExhibit : NSObject
@property(copy, nonatomic) NSString *name;
@property(copy, nonatomic) NSString *type;
@end

the JSON data should look like

[
  {"name":"Tennis Ball Launchers",
  "type":"Forces"},

  {"name":"Magnetic Hot Plates",
  "type":"Magnetism"},

  ...
]

Then you can create an object from a dictionary with

-(instancetype)initWithDictionary:(NSDictionary *)dict
{
    self = [super init];
    if (self) {
        [self setValuesForKeysWithDictionary:dict];
    }
    return self;
}

self.allExhibits = [[NSArray alloc] initWithArray:myItems];

can be simplified to

self.allExhibits = [myItems copy];

The complete code then looks like this (where I have renamed some variables, this might be a matter of personal taste):

// Read JSON data into array
NSError *error;
NSString *jsonPath = [[NSBundle mainBundle] pathForResource:@"test" ofType:@"json"];
NSData *jsonData = [NSData dataWithContentsOfFile:jsonPath];
if (jsonData == nil) {
    // handle error ...
}
NSArray *jsonArray = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error];
if (jsonArray == nil) {
    // handle error ...
}

// Create custom objects from JSON array
NSMutableArray *items = [[NSMutableArray alloc] init];
for (NSDictionary *dict in jsonArray) {
    W5BExhibit *exhibit = [[W5BExhibit alloc] initWithDictionary:dict];
    [items addObject:exhibit];
}
self.exhibits = [items copy];
\$\endgroup\$
  • \$\begingroup\$ Wow - that has given me a lot to think about. I appreciate it. \$\endgroup\$ – bunglenutter Oct 12 '14 at 10:23
2
\$\begingroup\$

This is fine, implementation.

  • NSItems is a poor variable name. See Cocoa Naming Basics.

  • NSItems and rawItems should be local variables, not a class properties. Keep your variables to the narrowest possible scope.

  • Instead of for loop with int i, you can fast enumerate through your array. Thus, instead of:

    for (int i = 0; i < [self.rawExhibits count]; i++) {
        NSDictionary *object = [self.rawExhibits objectAtIndex:i];
        NSString *objectName = object[@"Name"];
        NSString *objectType = object[@"Type"];
    
        // etc.
    }
    

    You could instead do:

    for (NSDictionary *object in self.rawExhibits) {
        NSString *objectName = object[@"Name"];
        NSString *objectType = object[@"Type"];
    
        // etc.
    }
    

But the basic approach of creating custom objects is a very solid notion.

Having said that, this seems more applicable when reading data from some web service. If you're really reading this from your bundle, I might be inclined to making my objects NSCoding compliant, and then save this collection of objects using a NSKeyedArchiver and then the app could instantiate the allExhibits with a single call to NSKeyedUnarchiver. See the Archives and Serializations Programming Guide for more information. Or Core Data is another approach.

This approach of parsing the JSON and then reprocessing the array seems cumbersome if dealing with some resource in your bundle.

\$\endgroup\$
  • \$\begingroup\$ Thanks for pointing all that out - it sped up the app considerably. Do you think archiving is worthwhile? \$\endgroup\$ – bunglenutter Oct 11 '14 at 20:42
  • \$\begingroup\$ Archives seem appealing because you bypass needing to stage the results in an array of dictionaries, and instead you save and load the dictionary of custom objects directly. But when I benchmarked it, it didn't appear to be any more efficient, so the benefits may be more stylistic than substantive. \$\endgroup\$ – Rob Oct 12 '14 at 2:23
  • \$\begingroup\$ I might do it anyway for the sake of practice. \$\endgroup\$ – bunglenutter Oct 12 '14 at 10:07
2
\$\begingroup\$

I suggest using NSJSONSerialization to serialize the JSON into a dictionary. Then, create custom objects with the appropriate properties and implement key value coding methods to populate those properties from the dictionary.

The useful methods are:

  • setValuesForKeysWithDictionary:
    • Pass your dictionary into this method to set up your custom object with the JSON's keys and values.
  • setValue:forKey:
    • Implement this method if you need to do any special mappings/transformations for a specific key.
  • setValue:forUndefinedKey:
    • Implement this method if you think the JSON might have a value that your object doesn't have a property for.

For more info, check out the references:

https://developer.apple.com/library/ios/documentation/foundation/reference/nsjsonserialization_class/

https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Protocols/NSKeyValueCoding_Protocol/Reference/Reference.html

\$\endgroup\$
  • \$\begingroup\$ +1 — this is exactly what I'd suggest; the W5BExhibit is created without a custom init and it exposes all the properties it expects to receive plus setValue:forUndefinedKey: silently to ignore missing properties. Then the outer loop, having found the relevant array within the incoming JSON, just creates an exhibit and calls setValuesForKeysWithDictionary: for each item in the array. \$\endgroup\$ – Tommy Oct 10 '14 at 23:35
  • \$\begingroup\$ Sorry - I'm having trouble visualising what the JSON above would look like in an NSDictionary? Would I have an NSDictionary of NSDictionary objects? \$\endgroup\$ – bunglenutter Oct 11 '14 at 12:00
  • \$\begingroup\$ Well, NSJSONSerialization would return an NSArray of NSDictionary objects based on the JSON in the question. You would then iterate through the array and create a custom object for every dictionary, passing the dictionary into that object's setValuesForKeysWithDictionary: method. \$\endgroup\$ – xytor Oct 12 '14 at 15:33
  • \$\begingroup\$ I think the penny has dropped. \$\endgroup\$ – bunglenutter Oct 12 '14 at 22:47
0
\$\begingroup\$

You implemented a good enough solution. You can improve the code a bit by adding initWithDictionary method to W5BExhibit and set values there.

When you parse the JSON you can get NSNull from the server. So, to avoid it you could use

#define SET_IF_NOT_NULL(TARGET, VALUE) if (VALUE != [NSNull null]) { TARGET = VALUE; } 

but it depends on the server implementation

\$\endgroup\$
  • \$\begingroup\$ I'd recommend something smarter, like AFNetworking's function AFJSONObjectByRemovingKeysWithNullValues(id JSONObject, NSJSONReadingOptions readingOptions). \$\endgroup\$ – jshier Oct 10 '14 at 21:34
  • \$\begingroup\$ it make sense if you plan to use it everywhere in your code. but add a heavy AFNetworking pod just to be able to use simple impl method. up to you \$\endgroup\$ – kabarga Oct 10 '14 at 21:41
  • \$\begingroup\$ That function can live on it's own if you want. Otherwise using a tried and tested library like AFNetworking is a good alternative to writing your own networking library. \$\endgroup\$ – jshier Oct 10 '14 at 22:04
  • 1
    \$\begingroup\$ I don't see a good reason to use a macro here (or anywhere, if at all possible). If you must do this, write a function, mark it inline, and trust the compiler. \$\endgroup\$ – sapi Oct 10 '14 at 22:04
  • \$\begingroup\$ Do I need to worry about NSNull of the JSON file is local? \$\endgroup\$ – bunglenutter Oct 11 '14 at 10:22
0
\$\begingroup\$

You can actually easily parse JSON with Objective-C - you'll end up with an _NSCFDictionary

NSData* data = [aJsonString dataUsingEncoding:NSUTF8StringEncoding];
NSObject* myJsonObject = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&error];

You can then reference objects by doing things like:

NSDictionary* anObject = (NSDictionary*)myJsonObject[@"aKey"];

or

NSArray* anArray = (NSArray*)myJsonObject[@"aKey"][@"anArray"];

or

NSString* aString = (NSString*)myJsonObject[@"aKey"][@"aString"];

Keep in mind if you're casting like this, you should know what structure you're expecting and handle errors in the cast if it doesn't work out. Conversely, you could not cast at all, and just try and reference the object you want, given the particular index path.

\$\endgroup\$
  • 1
    \$\begingroup\$ I wouldn't discourage the user from using custom objects. I think custom objects are much better than using dictionaries. The intent is much clearer, its more robust, you can add custom behaviors, etc. \$\endgroup\$ – Rob Oct 10 '14 at 21:57
  • 1
    \$\begingroup\$ Completely agree @Rob, there's a use for both. In this case it's such a simple dataset that this is entirely overkill, and specified objects makes it clearer to reference later. \$\endgroup\$ – remus Oct 10 '14 at 22:01

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.