iOS Programming: The Big Nerd Ranch Guide, 3/e (Big Nerd Ranch Guides) (93 page)

Read iOS Programming: The Big Nerd Ranch Guide, 3/e (Big Nerd Ranch Guides) Online

Authors: Aaron Hillegass,Joe Conway

Tags: #COM051370, #Big Nerd Ranch Guides, #iPhone / iPad Programming

BOOK: iOS Programming: The Big Nerd Ranch Guide, 3/e (Big Nerd Ranch Guides)
12.65Mb size Format: txt, pdf, ePub
JSON Serialization

So far,
Nerdfeed
deals with XML data only. While XML is nice, it is becoming less popular, and a new data format called JSON (
JavaScript object notation
) is coming into favor. Like XML, JSON is a human-readable data interchange format. However, JSON is a lot more concise, so it takes up less memory and is faster to transfer across networks.

 

There is a class that deals with JSON in iOS called
NSJSONSerialization
. This class can take a chunk of JSON data and turn it into objects. It can also go in the other direction: take objects and turn them into JSON data. Using this class means you don’t have to know with what JSON really looks like. (But, you should anyway, so be sure to read the section at the end of this chapter.)

 

The top songs RSS feed from Apple currently returns XML, but we can ask it to return JSON instead. Let’s do this to get a chance to try out
NSJSONSerialization
. In
BNRFeedStore.m
, change the service URL to request JSON.

 
- (void)fetchTopSongs:(int)count
        withCompletion:(void (^)(RSSChannel *obj, NSError *err))block
{
    
NSString *requestString = [NSString stringWithFormat:
        
@"http://itunes.apple.com/us/rss/topsongs/limit=%d/xml", count];
    
NSString *requestString = [NSString stringWithFormat:
        @"http://itunes.apple.com/us/rss/topsongs/limit=%d/json", count];
    NSURL *url = [NSURL URLWithString:requestString];
 

Of course, when this request completes,
BNRConnection
and
RSSChannel
will be pretty confused by this new type of data. Thus, we need to give
BNRConnection
the ability to determine whether the data is XML or JSON and parse it appropriately. We also need to teach
RSSChannel
and
RSSItem
how to gather their instance variables from JSON data.

 

First, here’s how
NSJSONSerialization
works. When JSON data is received, you send the message
JSONObjectWithData:options:error:
to the class
NSJSONSerialization
. This method takes the data and returns either an
NSArray
or an
NSDictionary
. Within this dictionary or array, there will be strings, numbers, or more dictionaries and arrays.

 

Each dictionary returned from
NSJSONSerialization
is an instance of
NSDictionary
and represents a single object within the JSON data. For example, the channel in the feed will be represented by an
NSDictionary
. This
NSDictionary
will contain an array of
NSDictionary
instances, and each dictionary will be an individual
RSSItem
.

 

While having the JSON data parsed into these common data structures is really convenient, it is sometimes useful to move this data into the appropriate model objects. In
Nerdfeed
, the
ListViewController
knows how to present
RSSChannel
and
RSSItem
s but not
NSArray
s and
NSDictionary
s. To keep things simple, we want to transfer the data from the dictionaries and arrays into our own classes,
RSSChannel
and
RSSItem
.

 

To do this, we will create a new protocol. This protocol will have one method,
readFromJSONDictionary:
, that takes an
NSDictionary
as an argument. A class that conforms to this protocol will implement
readFromJSONDictionary:
to retrieve its instance variables from the dictionary. Both
RSSChannel
and
RSSItem
will conform to this protocol. Create a new file from the Objective-C protocol template (
Figure 28.10
).

 

Figure 28.10  Creating an Objective-C protocol

 

Name this protocol
JSONSerializable
. In
JSONSerializable.h
, add a method to the protocol.

 
@protocol JSONSerializable
- (void)readFromJSONDictionary:(NSDictionary *)d;
@end
 

In a moment, you will write the code so that both
RSSChannel
and
RSSItem
conform to this protocol. Right now, though, let’s improve
BNRConnection
so that it can parse JSON data and pass the result to its root object. In
BNRConnection.h
, import the file
JSONSerializable.h
.

 
#import "JSONSerializable.h"

Then, add a new property to this class.

 
@property (nonatomic, strong) id xmlRootObject;
@property (nonatomic, strong) id jsonRootObject;
- (void)start;
 

When the
BNRFeedStore
creates a
BNRConnection
, it will either supply a
jsonRootObject
or an
xmlRootObject
to the connection, depending on the type of data it expects back from the server. When there is a
jsonRootObject
, the connection will parse the JSON data and hand the result off to the root object (instead of using
NSXMLParser
). In
BNRConnection.m
, synthesize this new property and modify the implementation of
connectionDidFinishLoading:
.

 
@synthesize jsonRootObject;
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    id rootObject = nil;
    if ([self xmlRootObject]) {
        NSXMLParser *parser = [[NSXMLParser alloc] initWithData:container];
        [parser setDelegate:[self xmlRootObject]];
        [parser parse];
        
        rootObject = [self xmlRootObject];
    }
else if ([self jsonRootObject]) {
        // Turn JSON data into basic model objects
        NSDictionary *d = [NSJSONSerialization JSONObjectWithData:container
                                                          options:0
                                                            error:nil];
        // Have the root object construct itself from basic model objects
        [[self jsonRootObject] readFromJSONDictionary:d];
        rootObject = [self jsonRootObject];
    }
    if ([self completionBlock])
        
[self completionBlock]([self xmlRootObject], nil);
        
[self completionBlock](rootObject, nil);
    [sharedConnectionList removeObject:self];
}
 

In
BNRFeedStore.m
, change the method
fetchTopSongs:withCompletion:
so that it supplies a
jsonRootObject
to the connection instead of an
xmlRootObject
.

 
BNRConnection *connection = [[BNRConnection alloc] initWithRequest:req];
[connection setCompletionBlock:block];
[connection setXmlRootObject:channel];
[connection setJsonRootObject:channel];
[connection start];
 

You can build the application to check for syntax errors. You will see one warning in
BNRFeedStore.m
that says
RSSChannel
is incompatible with
setJsonRootObject:
. That’s because
RSSChannel
doesn’t yet conform to
JSONSerializable
.

 

In
RSSChannel.h
, import this file and declare that
RSSChannel
conforms to this protocol.

 
#import "JSONSerializable.h"
@interface RSSChannel : NSObject , JSONSerializable
>

Now that
RSSChannel
and
RSSItem
conform to
JSONSerializable
, once the connection completes, they will pull their data from the dictionary when sent
readFromJSONDictionary:
.

 

The implementation of
readFromJSONDictionary:
should take values from the dictionary argument and move them into the instance variables of the
RSSChannel
. The channel needs to grab the title and all of the entries from the channel. In
RSSChannel.m
, implement this method to do just that.

 
- (void)readFromJSONDictionary:(NSDictionary *)d
{
    // The top-level object contains a "feed" object, which is the channel.
    NSDictionary *feed = [d objectForKey:@"feed"];
    // The feed has a title property, make this the title of our channel.
    [self setTitle:[feed objectForKey:@"title"]];
    // The feed also has an array of entries, for each one, make a new RSSItem.
    NSArray *entries = [feed objectForKey:@"entry"];
    for (NSDictionary *entry in entries) {
        RSSItem *i = [[RSSItem alloc] init];
        // Pass the entry dictionary to the item so it can grab its ivars
        [i readFromJSONDictionary:entry];
        [items addObject:i];
    }
}
 

Do the same thing in
RSSItem.h
.

 
#import "JSONSerializable.h"
@interface RSSItem : NSObject , JSONSerializable
>

And in
RSSItem.m
, grab the title and link from the entry dictionary.

 
- (void)readFromJSONDictionary:(NSDictionary *)d
{
    [self setTitle:[[d objectForKey:@"title"] objectForKey:@"label"]];
    // Inside each entry is an array of links, each has an attribute object
    NSArray *links = [d objectForKey:@"link"];
    if ([links count] > 1) {
        NSDictionary *sampleDict = [[links objectAtIndex:1]
                                            objectForKey:@"attributes"];
        // The href of an attribute object is the URL for the sample audio file
        [self setLink:[sampleDict objectForKey:@"href"]];
    }
}
 

You can now build and run the application. Once it starts running, flip to Apple’s RSS feed. Voilà! Now that the
RSSItem
is grabbing the link, you can tap an entry in the table view and listen to a sample clip. (The debugger will spit out a lot of angry nonsense about not finding functions or frameworks, but after a moment, it will shut up, and the clip will play.)

 

Notice that, in MVCS (and in MVC, too), model objects are responsible for constructing themselves given a stream of data. For example,
RSSChannel
conforms to both
NSXMLParserDelegate
and
JSONSerializable
– it knows how to pack and unpack itself from XML and JSON data. Also, think about
BNRItem
instances in
Homepwner
: they implemented the methods from
NSCoding
, so that they could be written to disk.

Other books

Cullen's Bride by Fiona Brand
The Perfect Poison by Amanda Quick
Off Limits by Sawyer Bennett
The Lavender Garden by Lucinda Riley
Rebel Lexis by Paul Alan
Rebound by Aga Lesiewicz
The Queen of Attolia by Megan Whalen Turner
How to Ruin My Teenage Life by Simone Elkeles