Networking Basics - The Core iOS Developer’s Cookbook, Fifth Edition (2014)

The Core iOS Developer’s Cookbook, Fifth Edition (2014)

Chapter 13. Networking Basics

As Internet-connected devices, the iPhone and its iOS family members are particularly well suited to retrieving remote data and accessing web-based services. Apple has lavished the platform with a solid grounding in all kinds of network computing and its supporting technologies. This chapter surveys basic techniques for network computing, from connectivity testing and web-based downloading to processing the traditional forms of data provided via web services.

Recipe: Checking Your Network Status

Networked applications need a live connection to communicate with the Internet or other nearby devices. Applications should know whether such a connection exists before reaching out to send or retrieve data. Checking the network status lets an application communicate with users and explain why certain functions might be disabled.

Apple has rejected and will continue to reject applications that do not check network status before providing download options to the user. Apple reviewers are trained to check whether you properly notify the user, especially in the case of network errors. Always verify network status and alert the user accordingly.

Apple also may reject applications based on “excessive data usage.” If you plan to stream large quantities of data, such as voice or video, in your application, you should test for the current connection type. Provide lower-quality data streams for users on a cell network connection and higher-quality data for users with a Wi-Fi connection. Apple has had little tolerance for applications that place high demands on cell network data. Keep in mind that unlimited data has given way to metered accounts in the United States. You can alienate your users as well as Apple by overusing cell networks.

iOS tests for the following configuration states: some (that is, any kind of) network connection available, Wi-Fi available, and cell service available. No App Store–safe application programming interfaces (APIs) allow the iPhone to test for Bluetooth connectivity at this time (although you can limit your application to run only on Bluetooth-enabled devices), nor can you check whether a user is roaming and utilizing a potentially expensive cellular network before offering data access.

The System Configuration framework offers network-checking functions. Among these, SCNetworkReachabilityCreateWithAddress tests whether an IP address is reachable. Recipe 13-1 shows a simple example of this test in action.

The networkAvailable method determines whether your device has outgoing connectivity, which it defines as having both access and a live connection. This method, based on Apple sample code, returns YES when the network is available and NO otherwise. The flags used here indicate both that the network is reachable (kSCNetworkFlagsReachable) and that no further connection is required (kSCNetworkFlagsConnectionRequired). Other flags you may use are as follows:

Image kSCNetworkReachabilityFlagsIsWWAN—Tests whether your user is using the carrier’s wireless wide area network (WWAN) or local Wi-Fi. When available via WWAN, the network can be reached via EDGE, GPRS, LTE, or another type of cell connection. When using a WWAN connection, you might want to use lightweight versions of your resources (for example, smaller versions of images or lower-bandwidth videos) due to the connection’s constricted or costly bandwidth.

Image kSCNetworkReachabilityFlagsConnectionOnTraffic—Specifies that addresses can be reached with the current network configuration but that a connection must first be established. Any actual traffic will initiate the connection.

Image kSCNetworkReachabilityFlagsIsDirect—Tells whether the network traffic goes through a gateway or arrives directly.

Evaluating whether connectivity code works, it is best to test on a variety of devices. The iPhone and cell-enabled iPads offer the most options. These devices provide both cell and Wi-Fi support, enabling you to confirm that the network remains reachable when using a cellular WWAN connection.

Test this code by toggling Wi-Fi and cell data off and on in the iPhone’s Settings app. A slight delay sometimes occurs when checking for network reachability, so design your applications accordingly. Let the user know what your code is up to during the check.

SCNetworkReachabilityGetFlags is a synchronous call that can block for a long period of time, particularly on Domain Name System (DNS) lookup if there is no connection. Never call this method from your main thread in production code. A long enough delay on the main thread will result in your app being booted by the iOS watchdog.

Use NSOperationQueue to move the blocking call off the main thread. Be sure to push any interaction with the UI back on to the main thread:

[[[NSOperationQueue alloc] init] addOperationWithBlock:
^{
// blocking call
BOOL networkAvailable = [device networkAvailable];

// UI interaction
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
textView.text = networkAvailable ? @"Yes" : @"No";
}];
}];

Indicate whether your application is using the network by setting the networkActivityIndicatorVisible property for the shared application instance. A spinning indicator in the status bar shows that network activity is in progress.

Recipe 13-1 Testing a Network Connection


SCNetworkReachabilityRef reachability;
SCNetworkConnectionFlags connectionFlags;

- (void)pingReachability
{
if (!reachability)
{
BOOL ignoresAdHocWiFi = NO;
struct sockaddr_in ipAddress;
bzero(&ipAddress, sizeof(ipAddress));
ipAddress.sin_len = sizeof(ipAddress);
ipAddress.sin_family = AF_INET;
ipAddress.sin_addr.s_addr =
htonl(ignoresAdHocWiFi ? INADDR_ANY : IN_LINKLOCALNETNUM);

reachability = SCNetworkReachabilityCreateWithAddress(
kCFAllocatorDefault, (struct sockaddr *)&ipAddress);
CFRetain(reachability);
}

// Recover reachability flags
BOOL didRetrieveFlags = SCNetworkReachabilityGetFlags(
reachability, &connectionFlags);
if (!didRetrieveFlags)
NSLog(@"Error. Could not recover network reachability flags");
}

- (BOOL)networkAvailable
{
[[UIApplication sharedApplication]
setNetworkActivityIndicatorVisible:YES];
[self pingReachability];
BOOL isReachable =
(connectionFlags & kSCNetworkFlagsReachable) != 0;
BOOL needsConnection =
(connectionFlags & kSCNetworkFlagsConnectionRequired) != 0;
[[UIApplication sharedApplication]
setNetworkActivityIndicatorVisible:NO];
return (isReachable && !needsConnection) ? YES : NO;
}}



Get This Recipe’s Code

To find this recipe’s full sample project, point your browser to https://github.com/erica/iOS-7-Cookbook and go to the folder for Chapter 13.


Scanning for Connectivity Changes

Connectivity state may change while an application is running. Checking once at application launch usually isn’t enough for an application that depends on data connections throughout its lifetime. When a network connection is lost or when it can finally be established, your UI should adjust accordingly, such as disabling or enabling a button or alerting the user.

Listing 13-1 addresses this challenge by extending the UIDevice reachability category to monitor network changes. It provides a pair of methods that allow you to schedule and unschedule reachability watchers—observers that notify when the connectivity state changes. It builds a callback that messages a watcher object when that state changes. The monitor is scheduled on the current run loop and runs asynchronously. Upon detecting a change, the callback function triggers.

Listing 13-1’s callback function redirects itself to a custom delegate method, reachability-Changed, which must be implemented by its watcher. That watcher object can then query for current network state.

The method that schedules the watcher assigns the delegate as its parameter. Here’s a trivial case of how that might be implemented skeletally, using Listing 13-1’s implementation. In real-world deployment, you’ll want to update the functionality presented in your GUI to match the availability (or lack thereof) of network-only features. Inform your user when connectivity changes and update your interface to mirror the current state. You might want to disable buttons or menu items that depend on network access when that access disappears. Providing an alert of some kind lets the user know why the GUI has updated.

Be prepared for multiple callbacks. Your application will generally receive one callback at a time for each kind of state change (that is, when the cellular data connection is established or released) or when Wi-Fi is established or lost. Your user’s connectivity settings (especially remembering and logging in to known Wi-Fi networks) will affect the kind and number of callbacks you may have to handle.

Listing 13-1 Monitoring Connectivity Changes


@protocol ReachabilityWatcher <NSObject>
- (void)reachabilityChanged;
@end

// For each callback, ping the watcher
static void ReachabilityCallback(
SCNetworkReachabilityRef target,
SCNetworkConnectionFlags flags, void* info)
{
@autoreleasepool {
id watcher = (__bridge id) info;
if ([watcher respondsToSelector: @selector(reachabilityChanged)])
[watcher performSelector: @selector(reachabilityChanged)];
}
}

// Schedule watcher into the run loop
- (BOOL)scheduleReachabilityWatcher:(id <ReachabilityWatcher>)watcher
{
[self pingReachability];

SCNetworkReachabilityContext context =
{0, (__bridge void *)watcher, NULL, NULL, NULL};
if(SCNetworkReachabilitySetCallback(reachability,
ReachabilityCallback, &context))
{
if(!SCNetworkReachabilityScheduleWithRunLoop(
reachability, CFRunLoopGetCurrent(),
kCFRunLoopCommonModes))
{
NSLog(@"Error: Could not schedule reachability");
SCNetworkReachabilitySetCallback(reachability, NULL, NULL);
return NO;
}
}
else
{
NSLog(@"Error: Could not set reachability callback");
return NO;
}
return YES;
}

// Remove the watcher
- (void)unscheduleReachabilityWatcher
{
SCNetworkReachabilitySetCallback(reachability, NULL, NULL);
if (SCNetworkReachabilityUnscheduleFromRunLoop(
reachability, CFRunLoopGetCurrent(),
kCFRunLoopCommonModes))
NSLog(@"Success. Unscheduled reachability");
else
NSLog(@"Error: Could not unschedule reachability");

CFRelease(reachability);
reachability = nil;
}


The URL Loading System

Apple provides a robust stack of APIs for communicating over the network. Each level of the stack is available to you, starting with the BSD Sockets layer at the base, the C-based CoreFoundation layer, and the Objective-C–based Foundation layer. For client-side apps, there is rarely a need to dive deeper than the Foundation layer.

For most applications, your network communication can be provided by the URL Loading System found in the Foundation layer. With this system, you connect, download, and upload data to many services that can be referenced via a URL. This functionality is not limited to HTTP-based services (http and https) but includes support for file transfer protocol (ftp), local file URLs, and data URLs.

The URL Loading System shipped with the initial iOS SDK, NSURLConnection (which is confusingly both the name of the technology as well as the name of the headlining class), provided the heavy network lifting for thousands of apps. This framework was initially built for the Safari browser and then migrated to Foundation.

With iOS 7, Apple radically overhauled NSURLConnection to be even more flexible, configurable, and robust. The new technology, NSURLSession, is a complete replacement that provides substantial improvements to the granularity of configuration, better authentication handling, a more convenient and richer delegate model, and easy access to the new background downloading also introduced in iOS 7. While the old NSURLConnection system is still available, there is no reason not to move to the new NSURLSession technology.

While many classes used in NSURLConnection are maintained in NSURLSession, such as NSURL, NSURLRequest, and NSURLResponse, a new set of classes provide additional support for configuration and the task of downloading or uploading data.

Configuration

Configuration of a session uses an NSURLSessionConfiguration object. Each session uses a separate configuration object, improving on the global configuration provided in the legacy NSURLConnection. You set properties for connection policies, number of connections, cell usage, cache, credentials, and cookie storage. Use one of the class factories to create a configuration and then modify appropriately.

One welcome new property is an explicit resource timeout in addition to the network timeout. The network timeout (timeoutIntervalForRequest), which existed as a request-level configuration in the past, specifies the timeout for the minimum frequency of incoming bytes of data. The new resource timeout (timeoutIntervalForResource) specifies the overall timeout for the entire transfer.

Once the NSURLSessionConfiguration object is configured, pass it to the constructor of your NSURLSession object. A copy of your NSURLSessionConfiguration is stored in the session. While the configuration object is mutable, once it is passed to your session, changes are ignored; make sure your configuration is set appropriately.

Tasks

Each unit of work in a session is defined by an NSURLSessionTask object. Tasks are a close corollary to the original NSURLConnection class, each representing a single network request. A task provides the current state of a request. Once active, you can use a task to cancel, suspend, or resume activity. Much of the connection state that required the implementation of a delegate to access in NSURLConnection are now available as properties on NSURLSessionTask.

NSURLSessionTask is an abstract class with three concrete classes provided for general data transfers, download, and upload functions, as shown in Figure 13-1 (left): NSURLDataTask, NSURLDownloadTask, and NSURLUploadTask. All these tasks support a convenient block-based handler as well as a more flexible delegate-based mechanism for performing and handling the network request.

Image

Figure 13-1 A family of session tasks provide the ability to download and upload data (left). An accompanying hierarchy of delegate protocols for the tasks as well as NSURLSession allow for accessing and responding to state changes (right).

The subclasses of NSURLSessionTask provide similar functionality, with a few important differences. Data tasks provide a general-purpose base class, supporting both the sending and receiving of data from memory. Upload tasks are identical to data tasks (they are actually a subclass ofNSURLSessionDataTask) but with an additional delegate call for status and the ability to be used in the background transfer functionality (see Recipe 13-5). Download tasks store the incoming data directly to a file and allow the resumption of a cancelled or failed download.

NSURLSession

In the new URL Loading System, sessions maintain the current configuration and serve as the factory for creating tasks. These long-lived objects are intended to be active over multiple network tasks. When creating a session, you can use the class constructors to retrieve the default session or create a custom session. The primary difference is that the default session uses a shared configuration (actually the same shared stack that the legacy NSURLConnection uses), whereas the custom session can be configured with your own customized, private configuration.

NSURLSession also maintains the single delegate that is used for callbacks from the entire stack of session classes, as shown in Figure 13-1 (right). This includes NSURLSessionDelegate, NSURLSessionTaskDelegate, NSURLSessionDataTaskDelegate,NSURLSessionDownloadTaskDelegate, and NSURLSessionUploadTaskDelegate. Delegate usage is addressed further in Recipe 13-3.

When you are done with a session, be sure to invalidate it with invalidateAndCancel, cancelling outstanding tasks immediately, or finishTasksAndInvalidate, which returns and waits for the last task to complete. After the tasks have been cancelled or finished, references to the delegate objects and callbacks will be severed and released. You cannot use a session object again once it’s invalidated.

Recipe: Simple Downloads

Many classes provide convenience methods that allow you to request data from the Internet, wait until that data is received, and then move on to the next step in the application. The following snippet is both synchronous and blocking:

- (UIImage *)imageFromURLString:(NSString *)urlstring
{
// This is a blocking call
return [UIImage imageWithData:[NSData
dataWithContentsOfURL:[NSURL URLWithString:urlstring]]];
}

You will not return from this method until all the data is received. If the connection hangs, so will your app. The iOS system watchdog will summarily terminate your app if it blocks the main thread for too long; it won’t just hang forever.

Do not use such convenience methods without moving them to a background thread (as shown in Recipe 13-1).

While these helper methods are quick and easy to use, they lack flexibility and control, such as tracking the progress of the download, suspending the transfer, or setting security credentials. Using NSURLSession is the preferred approach for general downloads with full control and configurability. You can use a series of delegate callbacks or a convenient block-based handler.

Recipe 13-2 focuses on the simpler, block-based approach. On your session, use the NSURLSessionDownloadTask factory method that accepts a completion handler:

NSURLSessionDownloadTask *task =
[session downloadTaskWithRequest:request
completionHandler:^(NSURL *location,
NSURLResponse *response,
NSError *error) { // do something }];

When the download finishes, the completion handler is passed the location of the downloaded file, a response object, and an error object. You can then process the downloaded file or move it to a more appropriate location. The initial location of the file is temporary; the file should not be used outside the handler.

Some Internet providers produce a valid web page, even when given a completely bogus URL. The data returned in the response parameter helps you determine when this happens. This parameter points to an NSURLResponse object. It stores information about the data returned by the URL connection. These parameters include expected content length and a suggested filename. If the expected content length is less than zero, that’s a good clue that the provider has returned data that does not match up to your expected request:

NSLog(@"Response expects %d bytes",
response.expectedContentLength);

Recipe 13-2 allows testing with three predefined URLs. There’s one that downloads a short (3 MB) movie, another using a larger (35 MB) movie, and a final fake URL to test errors. The movies are sourced from the Internet Archive (http://archive.org), which provides a wealth of public domain data.

With the large movie, you may wish to allow access only via a non-cellular connection. This can be configured using the NSURLSessionConfiguration object:

NSURLSessionConfiguration *configuration =
[NSURLSessionConfiguration defaultSessionConfiguration];
configuration.allowsCellularAccess = NO;

Pass this configuration when creating your NSURLSession:

NSURLSession *session =
[NSURLSession sessionWithConfiguration:configuration];

As you can see in Recipe 13-2, NSURLSessionDownloadTask with the completion handler provides no interdownload feedback. Recipe 13-3 addresses this issue by using the somewhat more complex but fully featured delegate mechanism.

Recipe 13-2 Simple Downloads


// Large Movie (35 MB)
#define LARGE_MOVIE @"http://www.archive.org/download/\
BettyBoopCartoons/Betty_Boop_More_Pep_1936_512kb.mp4"

// Short movie (3 MB)
#define SMALL_MOVIE @"http://www.archive.org/download/\
Drive-inSaveFreeTv/Drive-in--SaveFreeTv_512kb.mp4"

// Fake address
#define FAKE_MOVIE \
@"http://www.idontbelievethisisavalidurlforthisexample.com"

// Current URL to test
#define MOVIE_URL [NSURL URLWithString:LARGE_MOVIE]

// Location to copy the downloaded item
#define FILE_LOCATION [NSHomeDirectory()\
stringByAppendingString:@"/Documents/Movie.mp4"]

@interface TestBedViewController : UIViewController
@end

@implementation TestBedViewController
{
BOOL success;
MPMoviePlayerViewController *player;
}

- (void)playMovie
{
// Instantiate movie player with location of downloaded file
player = [[MPMoviePlayerViewController alloc]
initWithContentURL:[NSURL fileURLWithPath:FILE_LOCATION]];
[player.moviePlayer setControlStyle: MPMovieControlStyleFullscreen];
player.moviePlayer.movieSourceType = MPMovieSourceTypeFile;
player.moviePlayer.allowsAirPlay = YES;
[player.moviePlayer prepareToPlay];

// Listen for finish state
[[NSNotificationCenter defaultCenter] addObserverForName:
MPMoviePlayerPlaybackDidFinishNotification
object:player.moviePlayer queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification *notification)
{
[[NSNotificationCenter defaultCenter] removeObserver:self
name:MPMoviePlayerPlaybackDidFinishNotification
object:nil];
self.navigationItem.rightBarButtonItem.enabled = YES;
}];

[self presentMoviePlayerViewControllerAnimated:player];
}

// Perform an asynchronous download
- (void)downloadMovie:(NSURL *)url
{
// Turn on network activity indicator
[UIApplication sharedApplication].networkActivityIndicatorVisible
= YES;

NSDate *startDate = [NSDate date];

// Create a URL request with the URL to the movie
NSURLRequest *request = [NSURLRequest requestWithURL:url];

// Create a session configuration
NSURLSessionConfiguration *configuration =
[NSURLSessionConfiguration defaultSessionConfiguration];

// Turn off cellular access for this session
configuration.allowsCellularAccess = NO;

// Create a session with the custom configuration
NSURLSession *session =
[NSURLSession sessionWithConfiguration:configuration];

// Create a download task with the block-based convenience
// handler to fetch the data
NSURLSessionDownloadTask *task =
[session downloadTaskWithRequest:request
completionHandler:^(NSURL *location,
NSURLResponse *response, NSError *error) {

// Turn off the network activity indicator
[UIApplication sharedApplication]
.networkActivityIndicatorVisible = NO;

// Upon an error, reset the UI and abort.
if (error)
{
self.navigationItem.rightBarButtonItem.enabled = YES;
NSLog(@"Failed download.");
return;
}

// Copy temporary file
[[NSFileManager defaultManager] copyItemAtURL:location
toURL:[NSURL fileURLWithPath:FILE_LOCATION]
error:&error];

NSLog(@"Elapsed time: %0.2f seconds.",
[[NSDate date] timeIntervalSinceDate:startDate]);

// Play the movie
[self playMovie];
}];

// Begin the download task
[task resume];
}

// Respond to the user's request to play movie
- (void)action
{
self.navigationItem.rightBarButtonItem.enabled = NO;

// Stop any existing movie playback
[player.moviePlayer stop];
player = nil;

// Remove any existing data
if ([[NSFileManager defaultManager] fileExistsAtPath:FILE_LOCATION])
{
NSError *error;
if (![[NSFileManager defaultManager]
removeItemAtPath:FILE_LOCATION error:&error])
NSLog(@"Error removing existing data: %@",
error.localizedFailureReason);
}

// Fetch the data
[self downloadMovie:MOVIE_URL];
}

- (void)loadView
{
self.view = [[UIView alloc] init];
self.view.backgroundColor = [UIColor whiteColor];
self.navigationItem.rightBarButtonItem =
BARBUTTON(@"Play Movie", @selector(action));
}

@end



Get This Recipe’s Code

To find this recipe’s full sample project, point your browser to https://github.com/erica/iOS-7-Cookbook and go to the folder for Chapter 13.


Recipe: Downloads with Feedback

The block-based factory methods provided by NSURLSession create tasks with a convenient completion handler. When a task is completed, you can process your data and move on. While these methods are simple to use, sometimes you may need more interaction during your download or upload. NSURLSessionDelegate and its family of subprotocols provide more access and configurability to your tasks. The subprotocols include support for the parent NSURLSession-related delegate methods as well as callbacks specific to a general task, data task, or download task.

These delegates provide corresponding data about the progress or state of your session and tasks. Your NSURLSession object maintains a single, common delegate object that will respond to any of the session callbacks as well as possible task callbacks. This can seem a bit odd at first but realize that whatever delegate you assign to the NSURLSession object is responsible for responding to delegate methods for both the session and corresponding tasks.

To monitor the state of the download task, implement the URLSession:downloadTask: didWriteData:totalBytesWritten:totalBytesExpectedToWrite: delegate method. totalBytesWritten (number of total bytes transferred) andtotalBytesExpectedToWrite (expected number of bytes to be transferred) provide ample information to create a progress indicator in your user interface. You can also query the countOfBytesReceived and countOfBytesExptectedToReceive properties on the download task directly. Note the somewhat confusing use of written and received in these method signatures; each refers to the bytes transferred, and they should be interchangeable.

By dividing the bytes transferred by those expected to be transferred, you can derive a useful status string and compute a percentage that can be passed to a UIProgressView:

int64_t kilobytesReceived =
downloadTask.countOfBytesReceived / 1024;
int64_t kilobytesExpected =
downloadTask.countOfBytesExpectedToReceive / 1024;
NSString * statusString = [NSString stringWithFormat:@"%lldk of %lldk",
kilobytesReceived, kilobytesExpected];
double progress = (double)kReceived / (double)kExpected;
progressLabel.text = statusString;
[progressBarView setProgress:progress animated:YES];

In Recipe 13-3, a table provides a list of sizable movie downloads that can be downloaded by tapping on the table view cell. The navigation bar provides the live progress of the download and is updated in real time, as shown in Figure 13-2. At the completion of the download, the video can be viewed. While multiple downloads can be easily supported, the complexities in designing an instructional example while providing status on multiple downloads is a bit egregious. Recipe 13-3 restricts the user to a single download at a time.

Image

Figure 13-2 A table view tracks a list of download tasks and their current progress state.


Note

In iOS 7, Apple introduced NSProgress to provide generalized progress tracking and reporting. In Recipe 13-3, with single file downloads and basic status reporting, little tracking beyond the immediate update of the UI is required. This is easily provided by the download delegate callbacks, making the more robust and complex NSProgress unnecessary. NSProgress excels at tracking the overall state of a group of tasks, including tasks that provide less opportunity and access for tracking, as well as progress to UI elements through Key-value observing (KVO).


Recipe 13-3 Downloads with Feedback


// Helper class to hold information about a movie and its corresponding download
@interface MovieDownload : NSObject
@property (nonatomic, strong) NSURL *movieURL;
@property (nonatomic, strong) NSURLSessionDownloadTask *downloadTask;
@property (nonatomic, readonly) NSString *localPath;
@property (nonatomic, readonly) NSString *movieName;
@property (nonatomic, readonly) NSString *statusString;
@property (nonatomic, readonly) double progress;
- (instancetype)initWithURL:(NSURL *)movieURL
downloadTask:(NSURLSessionDownloadTask *)downloadTask;
@end

@implementation MovieDownload

- (instancetype)initWithURL:(NSURL *)movieURL
downloadTask:(NSURLSessionDownloadTask *)downloadTask
{
self = [super init];
if (self)
{
_movieURL = movieURL;
_downloadTask = downloadTask;
}
return self;
}

// A local file path for copying our temporary file
- (NSString *)localPath
{
NSString *localPath =
[NSString stringWithFormat:@"%@/Documents/%@",
NSHomeDirectory(), [self.movieURL lastPathComponent]];
return localPath;
}

// Display name in UI
- (NSString *)movieName
{
return [self.movieURL lastPathComponent];
}

// Status string based on progress from download task
- (NSString *)statusString
{
int64_t kReceived =
self.downloadTask.countOfBytesReceived / 1024;
int64_t kExpected =
self.downloadTask.countOfBytesExpectedToReceive / 1024;
NSString *statusString =
[NSString stringWithFormat:@"%lldk of %lldk",
kReceived, kExpected];
return statusString;
}

// Progress percentage from download task
- (double)progress
{
double progress = (double)self.downloadTask.countOfBytesReceived /
(double)self.downloadTask.countOfBytesExpectedToReceive;
return progress;
}
@end

// Large Movie (35 MB)
#define LARGE_MOVIE @"http://www.archive.org/download/\
BettyBoopCartoons/Betty_Boop_More_Pep_1936_512kb.mp4"

// Medium movie (8 MB)
#define MEDIUM_MOVIE @"http://www.archive.org/download/\
mother_goose_little_miss_muffet/\
mother_goose_little_miss_muffet_512kb.mp4"

// Short movie (3 MB)
#define SMALL_MOVIE @"http://www.archive.org/download/\
Drive-inSaveFreeTv/Drive-in--SaveFreeTv_512kb.mp4"

@interface TestBedViewController : UITableViewController
<NSURLSessionDownloadDelegate>
@end

@implementation TestBedViewController
{
NSMutableArray *movieDownloads;
NSURLSession *session;
UIProgressView *progressBarView;
MPMoviePlayerViewController *player;
BOOL downloading;
}

#pragma mark - NSURLSessionDownloadDelegate

// Handle download completion from the task
- (void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location
{
NSInteger index =
[self movieDownloadIndexForDownloadTask:downloadTask];
if (index < 0) return;
MovieDownload *movieDownload = movieDownloads[index];

// Copy temporary file
NSError *error;
[[NSFileManager defaultManager] copyItemAtURL:location
toURL:[NSURL fileURLWithPath:[movieDownload localPath]]
error:&error];
}

// Handle task completion
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error
{
if (error)
NSLog(@"Task %@ failed: %@", task, error);

// Update UI
[progressBarView setProgress:0 animated:NO];
self.navigationItem.title = @"";
downloading = NO;

// This method is called after didFinishDownloadingToURL
// Task state is up-to-date and reflects completion.
[self.tableView reloadData];
}

- (void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didResumeAtOffset:(int64_t)fileOffset
expectedTotalBytes:(int64_t)expectedTotalBytes
{
// Required delegate method
}

// Handle progress update from the task
- (void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didWriteData:(int64_t)bytesWritten
totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
NSInteger index =
[self movieDownloadIndexForDownloadTask:downloadTask];
if (index < 0) return;
MovieDownload *movieDownload = movieDownloads[index];

// Update UI
[progressBarView setProgress:movieDownload.progress animated:YES];
self.navigationItem.title = movieDownload.statusString;
}

#pragma mark - UITableViewDatasource

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
NSInteger sectionCount = 1;
return sectionCount;
}

- (NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section
{
NSInteger rowCount = movieDownloads.count;
return rowCount;
}

- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *cellIdentifier = @"CellIdentifier";
UITableViewCell *cell =
[tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (cell == nil)
{
cell = [[UITableViewCell alloc]
initWithStyle:UITableViewCellStyleSubtitle
reuseIdentifier:cellIdentifier];
}

// Reset the cell UI
cell.selectionStyle = UITableViewCellSelectionStyleDefault;
cell.textLabel.enabled = YES;
cell.detailTextLabel.enabled = YES;

// Set our text label to the file name
cell.textLabel.text = [movieDownloads[indexPath.row] movieName];

// Acquire the appropriate download task and check its state
NSURLSessionDownloadTask *downloadTask =
[movieDownloads[indexPath.row] downloadTask];
if (downloadTask.state == NSURLSessionTaskStateCompleted)
{
cell.detailTextLabel.text = @"Ready to Play";
}
else if (downloadTask.state == NSURLSessionTaskStateRunning)
{
cell.detailTextLabel.text = @"Downloading...";
}
else if (downloadTask.state == NSURLSessionTaskStateSuspended)
{
// If download already in progress, disable suspended cells.
if (downloading)
{
cell.selectionStyle = UITableViewCellSelectionStyleNone;
[cell.textLabel setEnabled:NO];
[cell.detailTextLabel setEnabled:NO];
}

if (downloadTask.countOfBytesReceived > 0)
{
cell.detailTextLabel.text = @"Download Paused";
}
else
{
cell.detailTextLabel.text = @"Not Started";
}
}

return cell;
}

#pragma mark - UITableViewDelegate

- (void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
// Acquire downloadTask and respond to user's selection
NSURLSessionDownloadTask *downloadTask =
[movieDownloads[indexPath.row] downloadTask];
if (downloadTask.state == NSURLSessionTaskStateCompleted)
{
// Download is complete. Play movie.
NSURL *movieURL =
[NSURL fileURLWithPath:[movieDownloads[indexPath.row]
localPath]];
[self playMovieAtURL:movieURL];
}
else if (downloadTask.state == NSURLSessionTaskStateSuspended)
{
// If suspended and not already downloading, resume transfer.
if (!downloading)
{
[downloadTask resume];
downloading = YES;
}
}
else if (downloadTask.state == NSURLSessionTaskStateRunning)
{
// If already downloading, pause the transfer.
[downloadTask suspend];
downloading = NO;
}
[tableView deselectRowAtIndexPath:indexPath animated:YES];
[tableView reloadData];
}

#pragma mark - Movie Download Handling & UI

// Helper method to get index of a movieDownload object from array
- (NSInteger)movieDownloadIndexForDownloadTask:
(NSURLSessionDownloadTask *)downloadTask
{
NSInteger foundIndex = -1;
NSInteger index = 0;
for (MovieDownload *movieDownload in movieDownloads)
{
if (movieDownload.downloadTask == downloadTask)
{
foundIndex = index;
break;
}
index++;
}
return foundIndex;
}

// Play movie at the provided URL
- (void)playMovieAtURL:(NSURL *)url
{
// Instantiate movie player with location of downloaded file
player =
[[MPMoviePlayerViewController alloc] initWithContentURL:url];
player.moviePlayer.controlStyle = MPMovieControlStyleFullscreen;
player.moviePlayer.movieSourceType = MPMovieSourceTypeFile;
player.moviePlayer.allowsAirPlay = YES;
[player.moviePlayer prepareToPlay];

[self presentMoviePlayerViewControllerAnimated:player];
}

// Convenience method to add movieDownload objects to our array
- (void)addMovieDownload:(NSString *)urlString
{
NSURL * url = [NSURL URLWithString:urlString];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLSessionDownloadTask *downloadTask =
[session downloadTaskWithRequest:request];

MovieDownload *movieDownload = [[MovieDownload alloc]
initWithURL:url downloadTask:downloadTask];
[movieDownloads addObject:movieDownload];
}

// Reset the UI, session, and tasks
- (void)reset
{
for (MovieDownload *movieDownload in movieDownloads)
{
// Cancel each task
NSURLSessionDownloadTask *downloadTask =
movieDownload.downloadTask;
[downloadTask cancel];

// Remove any existing data
if ([[NSFileManager defaultManager]
fileExistsAtPath:movieDownload.localPath])
{
NSError *error;
if (![[NSFileManager defaultManager]
removeItemAtPath:movieDownload.localPath
error:&error])
NSLog(@"Error removing existing data: %@",
error.localizedFailureReason);
}
}

// Cancel all tasks and invalidate session (releases delegate)
[session invalidateAndCancel];
session = nil;

// Create new configuration / session and set delegate
NSURLSessionConfiguration *sessionConfiguration =
[NSURLSessionConfiguration defaultSessionConfiguration];
session = [NSURLSession
sessionWithConfiguration:sessionConfiguration
delegate:self delegateQueue:[NSOperationQueue mainQueue]];

// Create the MovieDownload objects
movieDownloads = [[NSMutableArray alloc] init];
[self addMovieDownload:SMALL_MOVIE];
[self addMovieDownload:MEDIUM_MOVIE];
[self addMovieDownload:LARGE_MOVIE];

// Reset the UI
[progressBarView setProgress:0 animated:NO];
self.navigationItem.title = @"";
downloading = NO;
[self.tableView reloadData];
}

- (void)viewDidLoad
{
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
self.navigationItem.rightBarButtonItem =
BARBUTTON(@"Reset", @selector(reset));

// Set up the progress bar in the navigation bar
progressBarView = [[UIProgressView alloc]
initWithProgressViewStyle:UIProgressViewStyleBar];
progressBarView.frame = CGRectMake(0, 0,
self.navigationController.navigationBar.frame.size.width, 4);
[self.navigationController.navigationBar
addSubview:progressBarView];

[self reset];
}

@end



Get This Recipe’s Code

To find this recipe’s full sample project, point your browser to https://github.com/erica/iOS-7-Cookbook and go to the folder for Chapter 13.


Resuming Downloads

One neat feature of NSURLSessionDownloadTask is support for resuming downloads. If a download fails, you can query the download task for a resume data blob. Connection error callbacks provide this resume data in the [error userInfo] dictionary, with theNSURLSessionDownloadTaskResumeData key. You can also cancel your download task and receive the most current resume data blob via a block handler:

[downloadTask cancelByProducingResumeData: ^(NSData *resumeData) {
// save resume data
}];

When you are ready to resume your download, you can use the session downloadTaskWithResumeData: or downloadTaskWithResumeData:completionHandler:, passing in the saved resume data blob to create a new task that will continue the download near where the previous download task left off.

Recipe: Background Transfers

iOS 7 introduced an amazing feature that has been on many developers’ wish lists since the creation and availability of the SDK: the ability to continue and process a download or upload even when the application is in the background. Background transfers require the use of a delegate for event delivery. You can use either the upload or download tasks and their corresponding delegates. Background transfers are also limited to HTTP and HTTPS protocols. They can be created in the foreground or even when the app is already in the background.

In many ways, there is little difference between the creation and handling of an in-process transfer and of an out-of-process transfer. You set up your session and your download task and begin the network transfer.

The key to enabling background transfers is the configuration of your session. Use the backgroundSessionConfiguration: class constructor on NSURLSessionConfiguration, passing a unique identifier string that you will use in the future to reconnect with this session:

NSURLSessionConfiguration *configuration =
[NSURLSessionConfiguration
backgroundSessionConfiguration:@"CoreiOSBackgroundID"];
configuration.discretionary = YES;

session = [NSURLSession sessionWithConfiguration:configuration
delegate:self delegateQueue:nil];

The discretionary configuration property is available for background transfers to encourage them to occur when a device is plugged into power and on Wi-Fi.

When handling background transfers, this session setup needs to occur when you are establishing your application. If the app is launched after a crash or exit while a background transfer is proceeding, the re-creation of the session with the background ID will reestablish the background session that may be in progress. Delegate method calls will immediately begin firing for tasks associated with that session. You can also call getTasksWithCompletion-Handler: to receive all the existing background tasks directly.

If your application remains in the foreground, the progress and completion delegate methods will be called normally, and the transfer will resolve as a normal in-process download. If your application leaves the foreground, the download task will continue. Once completed, your app will be relaunched in the background.


Note

If your application suspends, exits, or even crashes, the background transfer will continue. Background transfers occur in a separate daemon process. The data will be available for your application on its next launch.


In your app delegate, you will implement application:handleEventsForBackgroundURLSession:completionHandler:. If your application is not running and your transfer requires an authorization request or when your tasks complete, your application will be launched in the background, and the application delegate method will be called.

Although your application is running in the background, your application UI is actually fully restored but hidden. You can update your UI based on the data just downloaded. An updated snapshot of your UI will be taken for use in the task switcher. To initiate this snapshot, call thecompletionHandler block passed to the app delegate method when you have finished handling your background tasks and updated your UI.

Recipe 13-4 takes the basic functionality of Recipe 13-2 and expands the download process to allow for background transfers. After the movie download is initiated, exiting or suspending the application will not stop the transfer. When the transfer completes, the movie will be saved and the UI prepared for the next foregrounding of the application. Unfortunately, the background transfer API does not allow you to bring your application into the foreground automatically. However, as shown in Recipe 13-4, it is possible to trigger a local notification.

Testing Background Transfers

To test, begin your download and tap the Home button on the device to suspend your app or use the Exit App button in the navigation bar. This button calls abort(), a function that immediately terminates the application. (Don’t ever use this function in a production application; it will be rejected in the app review process.)

Immediately relaunching the application after backgrounding or termination should bring up the app with the download still in progress. Following this same process without relaunching allows the transfer to complete in the background. When it is complete, the app is launched in the background, kicking off a local notification and an update of the UI in the task switcher.

Recipe 13-4 Background Transfers


// Notify the user of a background transfer completion
- (void)presentNotification
{
UILocalNotification *localNotification =
[[UILocalNotification alloc] init];
localNotification.alertBody = @"Download Complete!";
localNotification.alertAction = @"Background Transfer";
localNotification.soundName = UILocalNotificationDefaultSoundName;
localNotification.applicationIconBadgeNumber = 1;
[[UIApplication sharedApplication]
presentLocalNotificationNow:localNotification];
}

// Reset the application icon badge on activation
- (void)applicationDidBecomeActive:(UIApplication *)application
{
application.applicationIconBadgeNumber = 0;
}

// Handle the background transfer completion event
- (void)application:(UIApplication *)application
handleEventsForBackgroundURLSession:(NSString *)identifier
completionHandler:(void (^)())completionHandler
{
// Update the UI to make it apparent in the task switcher
tbvc.view.backgroundColor = [UIColor greenColor];
tbvc.statusLabel.text = @"BACKGROUND DOWNLOAD COMPLETED!";

// Present the local notification to the user
[self presentNotification];

// Update the task switcher snapshot
completionHandler();
}



Get This Recipe’s Code

To find this recipe’s full sample project, point your browser to https://github.com/erica/iOS-7-Cookbook and go to the folder for Chapter 13.


Web Services

It is becoming difficult to build an iOS app of any significance these days without interacting with one or more web services. Nearly all data available on the Internet, both public and private, is provided via a web service.

These utilitarian network endpoints generally use HTTP with messages encoded as JSON or XML. The APIs for these services are published publically for many sites. While many require registration to use, a few are still fully accessible. Private enterprise web services are only documented and accessible to those who have the correct authorization.

Apple provides the tools required to download and process most web services with ease. The URL Loading System gets and posts your data, and provided parsers can interpret and generate the messages passed back and forth.

Recipe: Using JSON Serialization

The NSJSONSerialization class is tremendously handy when you’re working with JSON-based web services. All you need is a valid JSON container (namely an array or a dictionary) whose components are also valid JSON objects, including strings, numbers, arrays, dictionaries, andNSNull. Test an object’s validity with isValidJSONObject, which returns YES if the object can be safely converted to JSON format:

// Build a basic JSON object
NSArray *array = @[@"Val1", @"Val2", @"Val3"];
NSDictionary *dict = @{@"Key 1":array,
@"Key 2":array, @"Key 3":array};

// Convert it to JSON
if ([NSJSONSerialization isValidJSONObject:dict])
{
NSData *data = [NSJSONSerialization
dataWithJSONObject:dict options:0 error:nil];
NSString *result = [[NSString alloc]
initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"Result: %@", result);
}

The code from this method produces the following JSON. Notice that dictionary output is not guaranteed to be in alphabetic order:

Result: {"Key 2":["Val1","Val2","Val3"],"Key 3":
["Val1","Val2","Val3"],"Key 1":["Val1","Val2","Val3"]}

Moving from JSON to a conforming object is just as easy. Recipe 13-5 uses JSONObjectWithData:options:error: to convert NSData representing a JSON object into an Objective-C representation. This recipe downloads the current weather forecast from the sitehttp://openweathermap.org, retrieves an array of forecasts for the next seven days from the returned dictionary, and uses it to power a standard table view.

Recipe 13-5 JSON Data


#define WXFORECAST @"http://api.openweathermap.org/data/2.5/\
forecast/daily?q=%@&units=Imperial&cnt=7&mode=json"
#define LOCATION @"Fairbanks"

// Return a cell for the index path
- (UITableViewCell *)tableView:(UITableView *)aTableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
// Top level dictionary for the forecast data
NSDictionary *top = [items objectAtIndex:indexPath.row];

// The date of this forecast under top
NSString *unixtime = top[@"dt"];

// The weather dictionary that includes the sky description
NSDictionary * weather = top[@"weather"][0];

// The sky description string under weather
NSString *wxDescription = weather[@"description"];

// Convert the unixtime to something we can use
NSDate *wxDate = [NSDate dateWithTimeIntervalSince1970:
[unixtime doubleValue]];

UITableViewCell *cell = [self.tableView
dequeueReusableCellWithIdentifier:@"cell"];
if (cell == nil)
{
cell = [[UITableViewCell alloc]
initWithStyle:UITableViewCellStyleSubtitle
reuseIdentifier:@"cell"];
}
cell.textLabel.text = wxDescription;
cell.detailTextLabel.text =
[dateFormatter stringFromDate:wxDate];
return cell;
}

#pragma mark - Web Service Download

- (void)loadWebService
{
self.title = LOCATION;

// Start the refresh control
[self.refreshControl beginRefreshing];

// Create the URL string based on location
NSString *urlString =
[NSString stringWithFormat:WXFORECAST, LOCATION];

// Set up the session
NSURLSessionConfiguration * configuration =
[NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session =
[NSURLSession sessionWithConfiguration:configuration];
NSURLRequest *request =
[NSURLRequest requestWithURL:[NSURL
URLWithString:urlString]];

// Create a data task to transfer the web service endpoint contents
NSURLSessionDataTask *dataTask =
[session dataTaskWithRequest:request
completionHandler:^(NSData *data,
NSURLResponse *response, NSError *error) {

// Stop the refresh control
[self.refreshControl endRefreshing];
if (error)
{
self.title = error.localizedDescription;
return;
}

// Parse the JSON from the data object
NSDictionary *json = [NSJSONSerialization
JSONObjectWithData:data options:0 error:nil];

// Store off the top level array of forecasts
items = json[@"list"];

[self.tableView reloadData];
}];

// Start the data task
[dataTask resume];
}



Get This Recipe’s Code

To find this recipe’s full sample project, point your browser to https://github.com/erica/iOS-7-Cookbook and go to the folder for Chapter 13.


Recipe: Converting XML into Trees

While many web services have moved to the simpler JSON format, XML is still a popular and powerful document encoding. iOS’s NSXMLParser class scans through XML, creating callbacks as new elements are processed and finished (that is, using the typical logic of a SAX parser). This class is terrific for when you’re downloading simple data feeds and want to scrape just a bit or two of relevant information. It might not be so great when you’re doing production-type work that relies on error checking, status information, and back-and-forth handshaking.

Recipe 13-6 retrieves the same weather forecast data from http://openweathermap.org as Recipe 13-5 but in XML format. It requests the xml mode rather than the json mode and uses an XML parser to populate its table:

#define WXFORECAST \
@"http://api.openweathermap.org/data/2.5/\
forecast/daily?q=%@&units=Imperial&cnt=7&mode=xml"
#define LOCATION @"Fairbanks"

- (void)loadWebService
{
self.title = LOCATION;

// Start the refresh control
[self.refreshControl beginRefreshing];

// Create the URL string based on location
NSString *urlString =
[NSString stringWithFormat:WXFORECAST, LOCATION];

// Set up the session
NSURLSessionConfiguration * configuration =
[NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session =
[NSURLSession sessionWithConfiguration:configuration];
NSURLRequest *request =
[NSURLRequest requestWithURL:[NSURL URLWithString:urlString]];

// Create a data task to transfer the web service endpoint contents
NSURLSessionDataTask *dataTask =
[session dataTaskWithRequest:request
completionHandler:^(NSData *data, NSURLResponse *response,
NSError *error) {

// Stop the refresh control
[self.refreshControl endRefreshing];
if (error)
{
self.title = error.localizedDescription;
return;
}

// Create the XML parser
XMLParser *parser = [[XMLParser alloc] init];

// Parse the XML from the data object
root = [parser parseXMLFromData:data];

// Store off the top level parent of forecasts
forecastsRoot = [root nodesForKey:@"forecast"][0];

[self.tableView reloadData];
}];

// Start the data task
[dataTask resume];
}

// Return a cell for the index path
- (UITableViewCell *)tableView:(UITableView *)aTableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
TreeNode *forecastRoot = forecastsRoot.children[indexPath.row];
NSString *day = [forecastRoot attributes][@"day"];
TreeNode *cloudsNode = [forecastRoot nodeForKey:@"clouds"];
NSString *wxDescription = [cloudsNode attributes][@"value"];

UITableViewCell *cell =
[self.tableView dequeueReusableCellWithIdentifier:@"cell"];
if (cell == nil)
{
cell = [[UITableViewCell alloc]
initWithStyle:UITableViewCellStyleSubtitle
reuseIdentifier:@"cell"];
}
cell.textLabel.text = wxDescription;
cell.detailTextLabel.text = day;
return cell;
}

Trees

Using tree data structures is an excellent way to represent XML data. They allow you to create search paths through data so that you can find just the data you’re looking for, as long as you can comfortably fit the data into memory. You can retrieve all elements, search for a success value, and so forth. Trees convert text-based XML back into a multidimensional structure.

To bridge the gap between NSXMLParser and tree-based parse results, you can use an NSXMLParser-based helper class to return more standard tree-based data. This requires a simple tree node like the kind shown here:

@interface TreeNode : NSObject
@property (nonatomic, weak) TreeNode *parent;
@property (nonatomic, strong) NSMutableArray *children;
@property (nonatomic, strong) NSString *key;
@property (nonatomic, strong) NSDictionary *attributes;
@property (nonatomic, strong) NSString *leafValue;
@end

This node uses double linking to access its parent and its children, allowing two-way traversal in a tree. Only parent-to-child values are retained, allowing the tree to deallocate without being explicitly torn down.

For example, take the following simplified XML snippet from the open http://weathermap.org web service:

<time day="2013-12-01">
<clouds value="sky is clear"/>
</time>

This valid XML will be parsed into two TreeNode objects with key values time and clouds. The time node’s children array will contain a clouds node. The clouds node will have a time node as a parent. The time node will have an attributes dictionary that includes aday key with the value 2013-12-01. Similarly, the clouds node will have an attributes dictionary with a value key and a corresponding value sky is clear.

Building a Parse Tree

Recipe 13-6 introduces the XMLParser class. Its job is to build a parse tree as the NSXMLParser class works its way through the XML source. The three standard NSXML routines (start element, finish element, and found characters) read the XML stream and perform a recursive depth-first descent through the tree.

The class adds new nodes when reaching new elements (parser:didStartElement: qualifiedName:attributes:) and adds leaf values when encountering text (parser:foundCharacters:). Because XML allows siblings at the same tree depth, this code uses a stack to keep track of the current path to the tree root. Siblings always pop back to the same parent in parser:didEndElement:, so they are added at the proper level.

After finishing the XML scan, the parseXMLFromData: method returns the root node.

Recipe 13-6 The XMLParser Helper Class


@implementation XMLParser
// Parser returns the tree root. Go down
// one node to the real results
- (TreeNode *)parse:(NSXMLParser *)parser
{
stack = [NSMutableArray array];
TreeNode *root = [TreeNode treeNode];
[stack addObject:root];

[parser setDelegate:self];
[parser parse];

// Pop down to real root
TreeNode *realRoot = [[root children] lastObject];

// Remove any connections
root.children = nil;
root.leafValue = nil;
root.key = nil;
realRoot.parent = nil;

// Return the true root
return realRoot;
}

- (TreeNode *)parseXMLFromURL:(NSURL *)url
{
TreeNode *results = nil;
@autoreleasepool {
NSXMLParser *parser =
[[NSXMLParser alloc] initWithContentsOfURL:url];
results = [self parse:parser];
}
return results;
}

- (TreeNode *)parseXMLFromData:(NSData *)data
{
TreeNode *results = nil;
@autoreleasepool {
NSXMLParser *parser =
[[NSXMLParser alloc] initWithData:data];
results = [self parse:parser];
}
return results;
}

// Descend to a new element
- (void)parser:(NSXMLParser *)parser
didStartElement:(NSString *)elementName
namespaceURI:(NSString *)namespaceURI
qualifiedName:(NSString *)qName
attributes:(NSDictionary *)attributeDict
{
if (qName) elementName = qName;

TreeNode *leaf = [TreeNode treeNode];
leaf.parent = [stack lastObject];
[(NSMutableArray *)[[stack lastObject] children] addObject:leaf];
leaf.attributes = attributeDict;
leaf.key = [NSString stringWithString:elementName];
leaf.leafValue = nil;
leaf.children = [NSMutableArray array];

[stack addObject:leaf];
}

// Pop after finishing element
- (void)parser:(NSXMLParser *)parser
didEndElement:(NSString *)elementName
namespaceURI:(NSString *)namespaceURI
qualifiedName:(NSString *)qName
{
[stack removeLastObject];
}

// Reached a leaf
- (void)parser:(NSXMLParser *)parser
foundCharacters:(NSString *)string
{
if (![[stack lastObject] leafValue])
{
[[stack lastObject]
setLeafValue:[NSString stringWithString:string]];
return;
}
[[stack lastObject] setLeafValue:
[NSString stringWithFormat:@"%@%@",
[[stack lastObject] leafValue], string]];
}
@end



Get This Recipe’s Code

To find this recipe’s full sample project, point your browser to https://github.com/erica/iOS-7-Cookbook and go to the folder for Chapter 13.


Summary

This chapter introduces basic network-supporting technologies. You have seen how to check for network connectivity, download data, and convert to and from JSON. Here are a few thoughts to take away with you from this chapter:

Image A portion of Apple’s networking support is provided through low-level C-based routines. If you can find a friendly Objective-C wrapper to simplify your programming work, consider using it. The only drawback occurs when you specifically need tight networking control at the most basic level of your application, which is rare. There are superb resources out there. Just search online for them.

Image The NSURLSession-based URL Loading System in iOS 7 provides a powerful new abstraction to downloading and uploading data from the Internet. Take advantage of this new API when possible.

Image The background transfer support in iOS 7 can be tremendously useful in providing a responsive and up-to-date user experience. When combined with the new silent push notifications (notifications that do not display alerts but can trigger download activity) and background fetch (a new background mode for frequent background downloads) also introduced in iOS 7, your application can always be current when the user launches.

Image The most important lesson about connecting from a device to the network is this: It can fail. Design your apps accordingly. Check for network connectivity, test for aborted downloads, and assume that data may arrive corrupted. Everything else follows from the basic fact that you cannot rely on data to arrive when you want, how you expect it to, and as you requested.

Image When working with networking, always think “threaded.” Naiveté in your approach to networking and the main thread will likely result in your application being summarily killed. Blocks and queues are your new best friends when it comes to creating positive user experiences in networked applications.