ios7网络连接

阅读更多

Note from Ray: This is an abbreviated version of a chapter from iOS 7 by Tutorials that we are releasing as part of the iOS 7 Feast. We hope you enjoy!

Each new iOS release contains some terrific new networking APIs, and iOS 7 is no exception. In iOS 7, Apple has introducedNSURLSession, which is a suite of classes that replacesNSURLConnection as the preferred method of networking.

In this NSURLSession tutorial, you will learn what this new class is, why and how you should use it, how it compares to what’s been before, and most importantly: get practice integrating it into a real app!

Note that this tutorial assumes you are familiar with basic networking concepts. If you are completely new to networking, you can still follow along as everything is step by step but there may be some concepts that are unfamiliar to you that you may have to look up along the way.

Why Use NSURLSession?

Why should you use NSURLSession? Well, it brings you a number of new advantages and benefits:

  • Background uploads and downloads: With just a configuration option when the NSURLSession is created, you get all the benefits of background networking. This helps with battery life, supports UIKit multitasking and uses the same delegate model as in-process transfers.
  • Ability to pause and resume networking operations: As you will see later, with the NSURLSession API any networking task can be paused, stopped, and restarted. No NSOperation sub-classing necessary.
  • Configurable container: Each NSURLSession is the configurable container for putting requests into. For example, if you need to set an HTTP header option you will only need to do this once and each request in the session will have the same configuration.
  • Subclassable and private storage: NSURLSession is subclassable and you can configure a session to use private storage on a per session basis. This allows you to have private storage objects outside of the global state.
  • Improved authentication handling: Authentication is done on a specific connection basis. When using NSURLConnection if an authentication challenge was issued, the challenge would come back for an arbitrary request, you wouldn’t know exactly what request was getting the challenge. With NSURLSession, the delegate handles authentication.

  • Rich delegate model: NSURLConnection has some asynchronous block based methods, however a delegate cannot be used with them. When the request is made it either works or fails, even if authentication was needed. With NSURLSession you can have a hybrid approach, use the asynchronous block based methods and also setup a delegate to handle authentication.
  • Uploads and downloads through the file system: This encourages the separation of the data (file contents) from the metadata (the URL and settings).

NSURLSession vs NSURLConnection

“Wow, NSURLSession sounds complicated!”, you might think. “Maybe I’ll just stick with my old friend NSURLConnection.”

Don’t worry – using NSURLSession is just as easy as using its predecessory NSURLConnection for simple tasks. For an example, let’s take a look at an example of making a simple network call to get JSON for the latest weather in London.

Assume you have this NSString for constructing the NSURL:

NSString *londonWeatherUrl =
  @"http://api.openweathermap.org/data/2.5/weather?q=London,uk";

First here is how you make this call when using NSURLConnection:

NSURLRequest *request = [NSURLRequest requestWithURL:
[NSURL URLWithString:londonWeatherUrl]];
 
[NSURLConnection sendAsynchronousRequest:request
   queue:[NSOperationQueue mainQueue]
   completionHandler:^(NSURLResponse *response,
                       NSData *data,
                       NSError *connectionError) {
      // handle response
}];

Now let’s use NSURLSession. Note that this is the simplest way to make a quick call usingNSURLSession. Later in the tutorial you will see how to configure the session and setup other features like delegation.

NSURLSession *session = [NSURLSession sharedSession];
[[session dataTaskWithURL:[NSURL URLWithString:londonWeatherUrl]
          completionHandler:^(NSData *data,
                              NSURLResponse *response,
                              NSError *error) {
            // handle response
 
  }] resume];

Notice that you do not need to specify what queue you are running on. Unless you specify otherwise, the calls will be made on a background thread. It might be hard to notice a difference between these two, which is by design. Apple mentions that the dataTaskWithURL is intended to replace sendAsynchronousRequest in NSURLConnection.

So basically – NSURLSession is just as easy to use as NSURLConnection for simple tasks, and has a rich extra set of functionality when you need it.

NSURLSession vs AFNetworking

No talk of networking code is complete without mentioning the AFNetworking framework. This is one of the most popular frameworks available for iOS / OS X, created by the brilliant Mattt Thompson.

Note: To learn more about AFNetworking, checkout the github page found at:  https://github.com/AFNetworking/AFNetworking. Also we have a tutorial for that:
http://www.raywenderlich.com/30445/afnetworking-crash-course

Here is what the code for the same data task would look like using AFNetworking 1.x:

NSURLRequest *request = [NSURLRequest requestWithURL:
                         [NSURL URLWithString:londonWeatherUrl]];
 
AFJSONRequestOperation *operation =
[AFJSONRequestOperation JSONRequestOperationWithRequest:request
    success:^(NSURLRequest *request,
              NSHTTPURLResponse *response,
              id JSON) {
    // handle response
} failure:nil];
[operation start];

One of the benefits of using AFNetworking is the data type classes for handling response data. Using AFJSONRequestOperation (or the similar classes for XML and plist) the success block has already parsed the response and returns the data for you. With NSURLSession you receive NSData back in the completion handler, so you would need to convert the NSData into JSON or other formats.

Note: You can easily convert NSData into JSON using the NSJSONSerialization class introduced in iOS 5. To learn more, check out Chapter 23 of  iOS 5 by Tutorials, “Working with JSON”.

So you might be wondering if you should use AFNetworking or just stick with NSURLSession.

Personally, I think that for simple needs it’s best to stick with NSURLSession – this avoids introducing an unnecessary third party dependency into your project. Also, with the new delegates, configuration, and task based API a lot of the “missing features” that AFNetworking added are now included.

However, if you would like to use some of the new 2.0 features found in AFNetworking like serialization and further UIKit integration (in addition to the UIImageView category) then it would be hard to argue against using it!

Note: In the 2.0 branch of AFNetworking, they have converted over to using  NSURLSession. See this post for more information:
https://github.com/AFNetworking/AFNetworking/wiki/AFNetworking-2.0-Migration-Guide

Introducing Byte Club

In this NSURLSession tutorial you’ll explore this new API by building a notes and picture-sharing app on top of the Dropbox Core API for a top secret organization named Byte Club.

So consider this tutorial your official invitation to Byte Club! What’s the first rule of Byte Club you might ask? No one talks about Byte Club — except for those cool enough to be reading this tutorial. And definitely not those Android users; they’re banned for life. :]

Head on in to the next section to get started building the app that will serve as your initiation into Byte Club.

Note that this tutorial assumes you have some basic familiarity with networking in previous versions if iOS. It’s helpful if you’ve used APIs like NSURLConnection or NSURLSession in the past. If you’re completely new to networking in iOS, you should check out our iOS Apprentice series for beginner developers before continuing with this tutorial.

Getting started

Byte Club is an exclusive group of iOS developers that joins together to perform coding challenges. Since each member works remotely on these challenges from across the world, members also find it fun to share panoramic photos of their “battle stations”.

Here’s a panoramic photo of Ray’s office setup, for example:

Note: You might want to create your own panoramic photo of your office – it’s fun, and it will come in handy later in this tutorial.

 

In iOS 7, you can take a panoramic photo by opening Photos and selecting the tab named Pano.

If you like the results, you can set it as the wallpaper for your lock screen by opening Settingsand selecting Brightness & Wallpaper \ Choose Wallpaper \ My Panoramas.

And of course – Byte Club has its own app to make this all happen. You can use the app to create coding challenges or share panoramic photos with other members. Behind the scenes, this is implemented with networking – specifically, by sharing files with the Dropbox API.

Starter project overview

First, download the starter project for this tutorial.

This starter project includes the UI pre-made for you, so you can focus on the networking part of the app in this tutorial. The starter project also includes some code to handle Dropbox authentication, which you’ll learn more about later on.

Open up the project in Xcode and run it up on your device or simulator. You should see a screen like this:

However, you won’t be able to log in yet – you have to configure the app first, which you’ll do in a bit.

Next open Main.storyboard and take a look at the overall design of the app:

This is a basic TabBarController app with two tabs: one for code challenges, and one for panoramic photos. There’s also a step beforehand that logs the user in to the app. You’ll set up the login after you create your Dropbox Platform App below.

Feel free to look through the rest of the app and get familiar with what’s there so far. You’ll notice that other than the authorization component, there’s no networking code to retrieve code challenges or panoramic photos – that’s your job!

Creating a new Dropbox Platform app

To get started with your Dropbox App, open the Dropbox App Console located at https://www.dropbox.com/developers/apps

Sign in if you have a Dropbox account, but if not, no sweat: just create a free Dropbox account. If this is your first time using the Dropbox API, you’ll need to agree to the Dropbox terms and conditions.

After the legal stuff is out of the way, choose the Create App option. You’ll be presented with a series of questions – provide the following responses

  • What type of app do you want to create?
    • Choose: Dropbox API app
  • What type of data does your app need to store on Dropbox?
    • Choose: Files and Datastore
  • Can your app be limited to its own, private folder?
    • Choose: No – My App needs access to files already on Dropbox
  • What type of files does your app need access to?
    • Choose: All File Types

 

Finally, provide a name for your app, it doesn’t matter what you choose as long as it’s unique. Dropbox will let you know if you’ve chosen a name that’s already in use. Your screen should look similar to the following:

ios7网络连接_第1张图片

Click Create App and you’re on your way!

The next screen you’ll see displays the screen containing the App key and App secret:

Don’t close this screen yet; you’ll need the App Key and App Secret for the next step.

Open Dropbox.m and find the following lines:

#warning INSERT YOUR OWN API KEY and SECRET HERE
static NSString *apiKey = @"YOUR_KEY";
static NSString *appSecret = @"YOUR_SECRET";

Fill in your app key and secret, and delete the #warning line. You can close the Dropbox Web App page at this point.

Next, create a folder in the root directory of your main Dropbox folder and name it whatever you wish. If you share this folder with other Dropbox users and send them a build of the Byte Club app, they will be able to create notes and upload photos for all to see.

Find the following lines in Dropbox.m:

#warning THIS FOLDER MUST BE CREATED AT THE TOP LEVEL OF YOUR DROPBOX FOLDER, you can then share this folder with others
NSString * const appFolder = @"byteclub";

Change the string value to the name of the Dropbox folder you created and delete the #warning pragma.

To distribute this app to other users and give them access tokens, you will need to turn on the “Enable additional users” setting for your Dropbox Platform App.

Go to the Dropbox app console at https://www.dropbox.com/developers/apps. Click on your app name, and then click the Enable Additional Users button. A dialog will appear stating that you have increased your user limit. Click Okay on the dialog to clear it. Your app page will now look like the following:

ios7网络连接_第2张图片

Note: You may notice that while you’re developing your app, you can give access to up to 100 users. When you’re ready to release your app for real, you have to apply for production status, which you can do by clicking the  Apply for production button and sending Dropbox some additional information.

 

Dropbox will then review your app to make sure it complies with their guidelines, and if all goes well they will then open your app’s API access to unlimited users.

Dropbox authentication: an overview

Before your app can use the Dropbox API, you need to authenticate the user. In Dropbox, this is done by OAuth – a popular open source protocol that allows secure authorization.

The focus of this tutorial is on networking, not OAuth, so I have already created a small API in Dropbox.m that handles most of this for you.

If you’ve ever used a third party twitter client app, like TweetBot, then you’ll be familiar with the OAuth setup process from a user’s perspective. The OAuth process is pretty much the same for your app.

Build and run your app, and follow the steps to log in. You will see a blank screen with two tabs, one for Notes and one for PanoPhotos, as shown below:

ios7网络连接_第3张图片

OAuth authentication happens in three high level steps:

  1. Obtain an OAuth request token to be used for the rest of the authentication process. This is the request token.
  2. A web page is presented to the user through their web browser. Without the user’s authorization in this step, it isn’t possible for your application to obtain an access token from step 3.
  3. After step 2 is complete, the application calls a web service to exchange the temporary request token (from step1) for a permanent access token, which is stored in the app.

Note: To keep this tutorial concise, we are not going to go into details about how Dropbox authentication works here. However, if you’d like to learn more check out the full version of this tutorial that is part of iOS 7 by Tutorials.

The NSURLSession suite of classes

Apple has described NSURLSession as both a new class and a suite of classes. There’s new tools to upload, download, handle authorization, and handle just about anything in the HTTP protocol.

Before you start coding, it’s important to have a good understanding of the major classes in theNSURLSession suite and how they work together.

ios7网络连接_第4张图片

An NSURLSession is made using an NSURLSessionConfiguration with an optional delegate. After you create the session you then satisfy you networking needs by creating NSURLSessionTask’s.

NSURLSessionConfiguration

There are three ways to create an NSURLSessionConfiguration:

  • defaultSessionConfiguration – creates a configuration object that uses the global cache, cookie and credential storage objects. This is a configuration that causes your session to be the most like NSURLConnection.
  • ephemeralSessionConfiguration – this configuration is for “private” sessions and has no persistent storage for cache, cookie, or credential storage objects.
  • backgroundSessionConfiguration – this is the configuration to use when you want to make networking calls from remote push notifications or while the app is suspended. Refer to Chapter 17 and 18 in iOS 7 by Tutorials, “Beginning and Intermediate Multitasking”, for more details.

Once you create a NSURLSessionConfiguration, you can set various properties on it like this:

NSURLSessionConfiguration *sessionConfig =
[NSURLSessionConfiguration defaultSessionConfiguration];
 
// 1
sessionConfig.allowsCellularAccess = NO;
 
// 2
[sessionConfig setHTTPAdditionalHeaders:
          @{@"Accept": @"application/json"}];
 
// 3
sessionConfig.timeoutIntervalForRequest = 30.0;
sessionConfig.timeoutIntervalForResource = 60.0;
sessionConfig.HTTPMaximumConnectionsPerHost = 1;
  1. Here you restrict network operations to wifi only.
  2. This will set all requests to only accept JSON responses.
  3. These properties will configure timeouts for resources or requests. Also you can restrict your app to only have one network connection to a host.

These are only a few of the things you can configure, be sure to check out the documentation for a full list.

NSURLSession

NSURLSession is designed as a replacement API for NSURLConnection. Sessions do all of their work via their minions, also known as NSURLSessionTask objects. With NSURLSession you can create the tasks using the block based convenience methods, setup a delegate, or both. For example, if you want to download an image (*challenge hint*), you will need to create an NSURLSessionDownloadTask.

First you need to create the session. Here’s an example:

// 1
NSString *imageUrl =
@"http://www.raywenderlich.com/images/store/iOS7_PDFonly_280@2x_authorTBA.png";
 
// 2
NSURLSessionConfiguration *sessionConfig =
  [NSURLSessionConfiguration defaultSessionConfiguration];
 
// 3
NSURLSession *session =
  [NSURLSession sessionWithConfiguration:sessionConfig
                                delegate:self
                           delegateQueue:nil];

Ok this is just a little different from what you have seen so far. Let’s go over it step by step.

  1. For this snippet we are downloading the same in two tasks.
  2. You always start by creating an NSURLConfiguration.
  3. This creates a session using the current class as a delegate.

After you create the session, you can then download the image by creating a task with a completion handler, like this:

// 1
NSURLSessionDownloadTask *getImageTask =
[session downloadTaskWithURL:[NSURL URLWithString:imageUrl]
 
    completionHandler:^(NSURL *location,
                        NSURLResponse *response,
                        NSError *error) {
        // 2
        UIImage *downloadedImage =
          [UIImage imageWithData:
              [NSData dataWithContentsOfURL:location]];
      //3
      dispatch_async(dispatch_get_main_queue(), ^{
        // do stuff with image
        _imageWithBlock.image = downloadedImage;
      });
}];
 
// 4
[getImageTask resume];

Ah ha! Now this looks like some networking code!

  1. Tasks are always created by sessions. This one is created with the block-based method. Remember you could still use the NSURLSessionDownloadDelegate to track download progress. So you get the best of both worlds! (*hint for challenge*)
    -URLSession:downloadTask
    :didWriteData:totalBytesWritten
    :totalBytesExpectedToWrite:
  2. Here you use the location variable provided in the completion handler to get a pointer to the image.
  3. Finally you could, for example, update UIImageView’s image to show the new file. (hint hint ☺)
  4. You always have to start up the task!
  5. Remember I said earlier that a session could also create tasks that will send messages to delegate methods to notify you of completion, etc.

Here is how that would look, using the same session from above:

// 1
NSURLSessionDownloadTask *getImageTask =
  [session downloadTaskWithURL:[NSURL URLWithString:imageUrl]];
 
[getImageTask resume];
  1. Well this is certainly less code ☺ However, if you only do this your never going to see anything.

You need to have your delegate implement some methods from the NSURLSessionDownloadDelegate protocol.

First we need to get notified when the download is complete:

-(void)URLSession:(NSURLSession *)session
     downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location
{
  // use code above from completion handler
}

Again you are provided with the location the file is downloaded to, and you can use this to work with the image.

Finally, if you needed to track the download progress, for either task creation method, you would need to use the following:

-(void)URLSession:(NSURLSession *)session
     downloadTask:(NSURLSessionDownloadTask *)downloadTask
     didWriteData:(int64_t)bytesWritten
totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
  NSLog(@"%f / %f", (double)totalBytesWritten,
    (double)totalBytesExpectedToWrite);
}

As you can see, NSURLSessionTask is the real workhorse for “getting stuff done” over the network.

NSURLSessionTask

So far you have seen NSURLSessionDataTask and NSURLSessionDownloadTask in use. Both of these tasks are derived from NSURLSessionTask the base class for both of these, are you can see here:

ios7网络连接_第5张图片

NSURLSessionTask is the base class for tasks in your session; they can only be created from a session and are one of the following subclasses.

NSURLSessionDataTask

This task issues HTTP GET requests to pull down data from servers. The data is returned in form of NSData. You would then convert this data to the correct type XML, JSON, UIImage, plist etc.

NSURLSessionDataTask *jsonData = [session dataTaskWithURL:yourNSURL
      completionHandler:^(NSData *data,
                          NSURLResponse *response,
                          NSError *error) {
        // handle NSData
}];

NSURLSessionUploadTask

Use this class when you need to upload something to a web service using HTTP POST or PUT commands. The delegate for tasks also allows you to watch the network traffic while it’s being transmitted.

Upload an image:

NSData *imageData = UIImageJPEGRepresentation(image, 0.6);
 
NSURLSessionUploadTask *uploadTask =
  [upLoadSession uploadTaskWithRequest:request
                              fromData:imageData];

Here the task is created from a session and the image is uploaded as NSData. There are also methods to upload using a file or a stream.

NSURLSessionDownloadTask

NSURLSessionDownloadTask makes it super-easy to download files from remote service and pause and resume the download at will. This subclass is a little different than the other two.

  • This type of task writes directly to a temporary file.
  • During the download the session will call URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite: to update status information
  • When the task is finished, URLSession:downloadTask:didFinishDownloadingToURL: is called. This is when you can save the file from the temp location to a permanent one.
  • When the download fails or is cancelled you can get the data to resume the download.

This feature will be terribly useful when downloading a Byte Club location panoramic photo to your device’s camera roll. You saw an example download task in the above snippet for downloading an image.

All of the above

All of the above tasks are created in a suspended state; after creating one you need to call its resume method as demonstrated below:

[uploadTask resume];

The taskIdentifier property allows you to uniquely identify a task within a session when you’re managing more than one task at a time.
That’s it! Now that you know the major classes in the NSURLSession suite, let’s try them out.

Sharing notes with NSURLSession

OK, this isn’t the Dead Poets Society, this is Byte Club! It’s time to start seeing some of this network code in action.

You need a way to send messages to other members of Byte Club. Since you’ve already set up an access token, the next step is to instantiate NSURLSesssion and make your first call to the Dropbox API.

Creating an NSURLSession

Add the following property to NotesViewController.m just after the NSArray *notes line:

@property (nonatomic, strong) NSURLSession *session;

You will create all of your minions from the session above.

Add the following method to NotesViewController.m just above initWithStyle:

- (id)initWithCoder:(NSCoder *)aDecoder
{
    self = [super initWithCoder:aDecoder];
    if (self) {
        // 1
        NSURLSessionConfiguration *config = [NSURLSessionConfiguration ephemeralSessionConfiguration];
 
        // 2
        [config setHTTPAdditionalHeaders:@{@"Authorization": [Dropbox apiAuthorizationHeader]}];
 
        // 3
        _session = [NSURLSession sessionWithConfiguration:config];
    }
    return self;
}

Here’s a comment-by-comment explanation of the code above:

  1. Your app calls initWithCoder when instantiating a view controller from a Storyboard; therefore this is the perfect spot to initialize and create the NSURLSession. You don’t want aggressive caching or persistence here, so you use the ephemeralSessionConfiguration convenience method, which returns a session with no persistent storage for caches, cookies, or credentials. This is a “private browsing” configuration.
  2. Next, you add the Authorization HTTP header to the configuration object. The apiAuthorizationHeader is a helper method I wrote that returns a string, in the OAuth specification format. This string contains the access token, token secret and your Dropbox App API key. Remember, this is necessary because every call to the Dropbox API needs to be authenticated.
  3. Finally, you create the NSURLSession using the above configuration.

This session is now ready to create any of the networking tasks that you need in your app.

GET Notes through the Dropbox API

To simulate a note being added by another user, add any text file of your choosing to the folder you set up in the root Dropbox folder. The example below shows the file test.txt sitting in the byteclubDropbox folder:

ios7网络连接_第6张图片

Wait until Dropbox confirms it has synced your file, then move on to the code below.

Add the code below to the empty notesOnDropBox method in NotesViewController.m:

[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
// 1
NSURL *url = [Dropbox appRootURL];
 
// 2
NSURLSessionDataTask *dataTask =
[self.session dataTaskWithURL:url
            completionHandler:^(NSData *data,
                                NSURLResponse *response,
                                NSError *error) {
    if (!error) {            
        // TODO 1: More coming here!
    }                
}];
 
// 3  
[dataTask resume];

The goal of this method is to retrieve a list of the files inside the app’s Dropbox folder. Let’s go over how this works section by section.

  1. In Dropbox, you can see the contents of a folder by making an authenticated GET request to a particular URL – like https://api.dropbox.com/1/metadata/dropbox/byteclub. I’ve created a convenience method in the Dropbox class to generate this URL for you.
  2. NSURLSession has convenience methods to easily create various types of tasks. Here you are creating a data task in order to perform a GET request to that URL. When the request completes, your completionHandler block is called. You’ll add some code here in a moment.
  3. Remember a task defaults to a suspended state, so you need to call the resume method to start it running.

That’s all you need to do to start a GET request – now let’s add the code to parse the results. Add the following lines right after the “TODO 1″ comment:

// 1
NSHTTPURLResponse *httpResp = (NSHTTPURLResponse*) response;
if (httpResp.statusCode == 200) {
 
    NSError *jsonError;
 
    // 2
    NSDictionary *notesJSON =
      [NSJSONSerialization JSONObjectWithData:data                                                                        
        options:NSJSONReadingAllowFragments                                                                             
        error:&jsonError];
 
    NSMutableArray *notesFound = [[NSMutableArray alloc] init];
 
    if (!jsonError) {                    
        // TODO 2: More coming here!
    }
}

There are two main sections here:

    1. You know you made a HTTP request, so the response will be a HTTP response. So here you cast the NSURLResponse to an NSHTTPURLRequest response so you can access to the statusCode property. If you receive an HTTP status code of 200 then all is well.

Example HTTP error codes:

400 – Bad input parameter. Error message should indicate which one and why.

401 – Bad or expired token. This can happen if the user or Dropbox revoked or expired an access token. To fix, you should re-authenticate the user.

403 – Bad OAuth request (wrong consumer key, bad nonce, expired timestamp…). Unfortunately, re-authenticating the user won’t help here.

404 – File or folder not found at the specified path.

405 – Request method not expected (generally should be GET or POST).

429 – Your app is making too many requests and is being rate limited. 429s can trigger on a per-app or per-user basis.

503 – If the response includes the Retry-After header, this means your OAuth 1.0 app is being rate limited. Otherwise, this indicates a transient server error, and your app should retry its request.

507 – User is over Dropbox storage quota.

5xx – Server error.

  1. The Dropbox API returns its data as JSON. So if you received a 200 response, then convert the data into JSON using iOS’s built in JSON deserialization. To learn more about JSON and NSJSONSerialization, check out Chapter 23 in iOS 5 by Tutorials, “Working with JSON.”

The JSON data returned from Dropbox will look something like this:

{
    "hash": "6a29b68d106bda4473ffdaf2e94c4b61",
    "revision": 73052,
    "rev": "11d5c00e1cf6c",
    "thumb_exists": false,
    "bytes": 0,
    "modified": "Sat, 10 Aug 2013 21:56:50 +0000",
    "path": "/byteclub",
    "is_dir": true,
    "icon": "folder",
    "root": "dropbox",
    "contents": [{
        "revision": 73054,
        "rev": "11d5e00e1cf6c",
        "thumb_exists": false,
        "bytes": 16,
        "modified": "Sat, 10 Aug 2013 23:21:03 +0000",
        "client_mtime": "Sat, 10 Aug 2013 23:21:02 +0000",
        "path": "/byteclub/test.txt",
        "is_dir": false,
        "icon": "page_white_text",
        "root": "dropbox",
        "mime_type": "text/plain",
        "size": "16 bytes"
    }],
    "size": "0 bytes"
}

So the last bit of code to add is the code that pulls out the parts you’re interested in from the JSON. In particular, you want to loop through the “contents” array for anything where “is_dir” is set to false.

To do this, add the following lines right after the “TODO 2″ comment:

// 1
NSArray *contentsOfRootDirectory = notesJSON[@"contents"];
 
for (NSDictionary *data in contentsOfRootDirectory) {
    if (![data[@"is_dir"] boolValue]) {
        DBFile *note = [[DBFile alloc] initWithJSONData:data];
        [notesFound addObject:note];
    }
}
 
[notesFound sortUsingComparator:
  ^NSComparisonResult(id obj1, id obj2) {
    return [obj1 compare:obj2];                    
}];
 
self.notes = notesFound;
 
// 6
dispatch_async(dispatch_get_main_queue(), ^{
    [UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
    [self.tableView reloadData];
});

There are two sections here:

  1. You pull out the array of objects from the “contents” key and then iterate through the array. Each array entry is a file, so you create a corresponding DBFile model object for each file.
    DBFile is a helper class I’ve created for you that pulls out the information for a file from the JSON dictionary – take a quick peek so you can see how it works.
    When you’re done, you add all the notes into the self.notes property. The table view is set up to display any entries in this array.

Now that you have the table view’s datasource updated, you need to reload the table data. Whenever you’re dealing with asynchronous network calls, you have to make sure to update UIKit on the main thread.

Astute readers will notice there’s no error handling in the code above; if you’re feeling like a keener (and most members of Byte Club are!) add some code here (and in subsequent code blocks you’ll add) that will retry in the case of an error and alert the user.

Build and run your app; you should see the file you added to your Dropbox folder show up in the list, as shown in the example below:

ios7网络连接_第7张图片

It’s a small thing, but it’s living proof that you’re calling the Dropbox API correctly.

The next step is to post notes and issue challenges to other club members, once again using the Dropbox API as your delivery mechanism.

POST Notes through the Dropbox API

Tap the plus sign in the upper right corner and you’ll see the Note add/edit screen appear, as illustrated below:

The starter app is already set up to pass the DBFile model object to the NoteDetailsViewControllerin prepareForSegue:sender:.

If you take a peek at this method, you’ll see that NoteViewController is set as the NoteDetailsViewController’s delegate. This way, the NoteDetailsViewController can notify NoteViewController when the user finishes editing a note, or cancels editing a note.

Open NotesViewController.m and add the following line to prepareForSegue:sender:, just after the line showNote.delegate = self;

showNote.session = _session;

NoteDetailsViewController already has an NSURLSession property named session, so you can set that in prepareForSegue:sender: before it loads.

Now the detail view controller will share the same NSURLSession, so the detail view controller can use it to make API calls to DropBox.

The Cancel and Done buttons are already present in the app; you just need to add some logic behind them to save or cancel the note that’s in progress.
In NoteDetailsViewController.m, find the following line in (IBAction)done:(id)sender:

// - UPLOAD FILE TO DROPBOX - //
    [self.delegate noteDetailsViewControllerDoneWithDetails:self];

…and replace it with the following:

// 1
NSURL *url = [Dropbox uploadURLForPath:_note.path];
 
// 2
NSMutableURLRequest *request = 
  [[NSMutableURLRequest alloc] initWithURL:url];
[request setHTTPMethod:@"PUT"];
 
// 3
NSData *noteContents = [_note.contents dataUsingEncoding:NSUTF8StringEncoding];
 
// 4
NSURLSessionUploadTask *uploadTask = [_session      
  uploadTaskWithRequest:request                                                                  
  fromData:noteContents                                                        
  completionHandler:^(NSData *data,                                                                             
  NSURLResponse *response,                                                                               
  NSError *error) 
{   
   NSHTTPURLResponse *httpResp = (NSHTTPURLResponse*) response;
 
   if (!error && httpResp.statusCode == 200) {
 
       [self.delegate noteDetailsViewControllerDoneWithDetails:self];
   } else {
       // alert for error saving / updating note
   }
}];
 
// 5
[uploadTask resume];

This implements everything you need to save and share your notes. If you take a close look at each commented section, you’ll see that the code does the following:

  1. To upload a file to Dropbox, again you need to use a certain API URL. Just like before when you needed a URL to list the files in a directory, I have created a helper method to generate the URL for you. You call this here.
  2. Next up is your old friend NSMutableURLRequest. The new APIs can use both plain URLs andNSURLRequest objects, but you need the mutable form here to comply with the Dropbox API wanting this request to be a PUT request. Setting the HTTP method as PUT signals Dropbox that you want to create a new file.
  3. Next you encode the text from your UITextView into an NSData Object.
  4. Now that you’re created the request and NSData object, you next create an NSURLSessionUploadTask and set up the completion handler block. Upon success, you call the delegate method noteDetailsViewControllerDoneWithDetails: to close the modal content. In a production-level application you could pass a new DBFile back to the delegate and sync up your persistent data. For the purposes of this application, you simply refresh the NotesViewController with a new network call.
  5. Again, all tasks are created as suspended so you must call resume on them to start them up. Lazy minions!

Build and run your app and tap on the plus sign of the Notes tab. Enter your name in the challenge name field, and enter some text in the note field that offers up a challenge to Ray, similar to the example below:

When you tap Done, the NoteViewController will return and list your new note as shown below:

ios7网络连接_第8张图片

You’ve officially thrown down the gauntlet and issued a challenge to Ray; however, he has friends in very high places so you’d better bring your best game!

But there’s one important feature missing. Can you tell what it is?

Tap on the note containing the challenge; the NoteDetailsViewController presents itself, but the text of the note is blank.

Ray won’t find your challenge very threatening if he can’t read it!

Right now, the app is only calling the Dropbox metadata API to retrieve lists of files. You’ll need to add some code to fetch the contents of the note.

Open NoteDetailsViewController.m and replace the blank retreiveNoteText implementation with the following:

-(void)retreiveNoteText
{
    // 1
    NSString *fileApi = 
      @"https://api-content.dropbox.com/1/files/dropbox";
    NSString *escapedPath = [_note.path 
      stringByAddingPercentEscapesUsingEncoding:
      NSUTF8StringEncoding];
 
    NSString *urlStr = [NSString stringWithFormat: @"%@/%@",
      fileApi,escapedPath];
 
    NSURL *url = [NSURL URLWithString: urlStr];
 
    [UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
 
    // 2
    [[_session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
 
        if (!error) {
            NSHTTPURLResponse *httpResp = (NSHTTPURLResponse*) response;
            if (httpResp.statusCode == 200) {
                // 3
                NSString *text = 
                 [[NSString alloc]initWithData:data 
                   encoding:NSUTF8StringEncoding];
                dispatch_async(dispatch_get_main_queue(), ^{
                    [UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
                    self.textView.text = text;
                });
 
            } else {
                // HANDLE BAD RESPONSE //
            }
        } else {
            // ALWAYS HANDLE ERRORS :-] //
        }
        // 4
    }] resume];
}

The code above (sans error checking) is explained in the notes below:

  1. Set the request path and the URL of the file you wish to retrieve; the /files endpoint in the Dropbox API will return the contents of a specific file.
  2. Create the data task with a URL that points to the file of interest. This call should be starting to look quite familiar as you go through this app.
  3. If your response code indicates that all is good, set up the textView on the main thread with the file contents you retrieved in the previous step. Remember, UI updates must be dispatched to the main thread.
  4. As soon as the task is initialized, call resume. This is a little different approach than before, as resume is called directly on the task without assigning anything.

Build and run your app, tap on your challenge in the list and the contents now display correctly in the view, as shown below:

ios7网络连接_第9张图片

You can play the part of Ray and respond to the challenge by entering text to the note; the files will be updated as soon as you tap Done.

Share the app and the Dropbox folder with some of your coding friends and have them test out your app by adding and editing notes between each other. After all, Byte Club is much more fun with more than one person in it!

Posting photos with NSURLSessionTask delegates

You’ve seen how to use NSURLSession asynchronous convenience methods. But what if you want to keep an eye on a file transfer, such as uploading a large file and showing a progress bar?

For this type of asynchronous, time-consuming task you’ll need to implement protocol methods from NSURLSessionTaskDelegate. By implementing this method, you can receive callbacks when a task receives data and finishes receiving data.

You might have noticed that the PanoPhotos tab is empty when you launch the app. However, the founding members of Byte Club have generously provided some panoramic photos of their own to fill your app with.

Download these panoramic photos that we have put together for you. Unzip the file, and copy the photos directory to your app folder on Dropbox. Your folder contents should look similar to the following:

The Dropbox Core API can provide thumbnails for photos; this sounds like the perfect thing to use for a UITableView cell.

Open PhotosViewController.m and add the following code to tableView:cellForRowAtIndexPath: just after the comment stating “GO GET THUMBNAILS”:

[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
NSURLSessionDataTask *dataTask = [_session dataTaskWithURL:url
  completionHandler:^(NSData *data, NSURLResponse *response, 
  NSError *error) {
    if (!error) {
      UIImage *image = [[UIImage alloc] initWithData:data];
      photo.thumbNail = image;
      dispatch_async(dispatch_get_main_queue(), ^{
        [UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
        cell.thumbnailImage.image = photo.thumbNail;
      });
    } else {
      // HANDLE ERROR //
    }
}];
[dataTask resume];

The above code displays the photo’s thumbnail image in the table view cell…or at least it would, if the _photoThumbnails array wasn’t currently empty.

Find refreshPhotos and replace its implementation with the following:

- (void)refreshPhotos
{
    [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];
    NSString *photoDir = [NSString stringWithFormat:@"https://api.dropbox.com/1/search/dropbox/%@/photos?query=.jpg",appFolder];
    NSURL *url = [NSURL URLWithString:photoDir];
 
    [[_session dataTaskWithURL:url completionHandler:^(NSData 
      *data, NSURLResponse *response, NSError *error) {
        if (!error) {
            NSHTTPURLResponse *httpResp = 
             (NSHTTPURLResponse*) response;
            if (httpResp.statusCode == 200) {
 
                NSError *jsonError;
                NSArray *filesJSON = [NSJSONSerialization  
                  JSONObjectWithData:data                                                                     
                  options:NSJSONReadingAllowFragments                                                                           
                  error:&jsonError];
                NSMutableArray *dbFiles = 
                  [[NSMutableArray alloc] init];
 
                if (!jsonError) {
                    for (NSDictionary *fileMetadata in 
                      filesJSON) {
                        DBFile *file = [[DBFile alloc] 
                          initWithJSONData:fileMetadata];
                        [dbFiles addObject:file];
                    }
 
                    [dbFiles sortUsingComparator:^NSComparisonResult(id obj1, id obj2) {
                        return [obj1 compare:obj2];
                    }];
 
                    _photoThumbnails = dbFiles;
 
                    dispatch_async(dispatch_get_main_queue(), ^{
                        [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
                        [self.tableView reloadData];
                    });
                }
            } else {
                // HANDLE BAD RESPONSE //
            }
        } else {
            // ALWAYS HANDLE ERRORS :-] //
        }
    }] resume];
}

This is very similar to the code you wrote earlier that loads the challenge notes. This time, the API call looks in the photos directory and only requests files with the .jpg extension.

Now that the _photoThumbnails array is populated, the thumbnail images will appear in the table view and update asynchronously.

Build and run your app and switch to the PanoPhotos tab; the thumbnails will load and appear as follows:

The photos look great — just beware of Matthijs’s code-shredding cat!

Upload a PanoPhoto

Your app can download photos, but it would be great if it could also upload images and show the progress of the upload.

To track the progress of an upload, the PhotosViewController must be a delegate for both theNSURLSessionDelegate and NSURLSessionTaskDelegate protocols so you can receive progress callbacks.

Modify the PhotosViewController interface declaration in PhotosViewController.m by adding
NSURLSessionTaskDelegate, as below:

@interface PhotosViewController ()

Next, add the following private property:

@property (nonatomic, strong) 
  NSURLSessionUploadTask *uploadTask;

The above pointer references the task object; that way, you can access the object’s members to track the progress of the upload task.

When the user chooses a photo to upload, didFinishPickingMediaWithInfo calls uploadImage: to perform the file upload. Right now, that method’s empty – it’s your job to flesh it out.

Replace uploadImage: with the following code:

- (void)uploadImage:(UIImage*)image
{
    NSData *imageData = UIImageJPEGRepresentation(image, 0.6);
 
    // 1
    NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
    config.HTTPMaximumConnectionsPerHost = 1;
    [config setHTTPAdditionalHeaders:@{@"Authorization": [Dropbox apiAuthorizationHeader]}];
 
    // 2
    NSURLSession *upLoadSession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
 
    // for now just create a random file name, dropbox will handle it if we overwrite a file and create a new name..
    NSURL *url = [Dropbox createPhotoUploadURL];
 
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
    [request setHTTPMethod:@"PUT"];
 
    // 3
    self.uploadTask = [upLoadSession uploadTaskWithRequest:request fromData:imageData];
 
    // 4
    self.uploadView.hidden = NO;
    [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];
 
    // 5
    [_uploadTask resume];
}

Here’s what’s going on in the code above:

  1. Previously, you used the session set up in initWithCoder and the associated convenience methods to create asynchronous tasks. This time, you’re using an NSURLSessionConfiguration that only permits one connection to the remote host, since your upload process handles just one file at a time.
  2. The upload and download tasks report information back to their delegates; you’ll implement these shortly.
  3. Here you set the uploadTask property using the JPEG image obtained from the UIImagePicker.
  4. Next, you display the UIProgressView hidden inside of PhotosViewController.
  5. Start the task — er, sorry, resume the task.

Now that the delegate has been set, you can implement the NSURLSessionTaskDelegate methods to update the progress view.

Add the following code to the end of PhotosViewController.m:

#pragma mark - NSURLSessionTaskDelegate methods
 
- (void)URLSession:(NSURLSession *)session 
  task:(NSURLSessionTask *)task 
  didSendBodyData:(int64_t)bytesSent 
  totalBytesSent:(int64_t)totalBytesSent 
  totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend
{
    dispatch_async(dispatch_get_main_queue(), ^{
        [_progress setProgress:
          (double)totalBytesSent / 
          (double)totalBytesExpectedToSend animated:YES];
    });
}

The above delegate method periodically reports information about the upload task back to the caller. It also updates UIProgressView (_progress) to show totalBytesSent / totalBytesExpectedToSend which is more informative (and much geekier) than showing percent complete.

The only thing left is to indicate when the upload task is complete. Add the following method to the end of PhotosViewController.m:

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
    // 1
    dispatch_async(dispatch_get_main_queue(), ^{
        [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
        _uploadView.hidden = YES;
        [_progress setProgress:0.5];
    });
 
    if (!error) {
        // 2
        dispatch_async(dispatch_get_main_queue(), ^{
            [self refreshPhotos];
        });
    } else {
        // Alert for error
    }
}

There’s not a lot of code here, but it performs two important tasks:

  1. Turns off the network activity indicator and then hides the _uploadView as a bit of cleanup once the upload is done.
  2. Refresh PhotosViewController to include the image you just uploaded since your demo app is not storing anything locally. In a real world app, you should probably be storing or caching the images locally.

Build and run your app, navigate to the PanoPhotos tab and tap the camera icon to select an image.

 

Note: If you’re using the simulator to test the app, you obviously can’t take a photo with your Mac, so just copy a panoramic photo to the simulator and upload that instead. To do this, ensure no other Xcode project is currently connected to the simulator and in Xcode select  Xcode \ Open Developer Tool \ iOS Simulator.

 

Drag one of the included panoramic photos from Finder to the simulator where the image will open in Safari. Then long press on the image and save the image to the photo library.

After selecting the image to upload, the uploadView displays in the middle of the screen along with the UIProgressView as shown below:

You might have noticed that an image upload can take some time due to the “better quality” scaling factor set on the upload task. Believe it or not, some users get a little impatient on their mobile devices! ☺ For those A-type personalities, you should provide a cancel function if the upload is taking too long.

The Cancel button on the uploadView has already been wired up from the storyboard, so you’ll just need to implement the logic to cleanly kill the download.

Replace the cancelUpload: method in PhotosViewController.m with the code below:

- (IBAction)cancelUpload:(id)sender {    
    if (_uploadTask.state == NSURLSessionTaskStateRunning) {
        [_uploadTask cancel];
    }
}

To cancel a task it’s as easy as calling the cancel method, which you can see here.

Now build and run your app, select a photo to upload and tap Cancel. The image upload will halt and the uploadView will be hidden.

And that’s it – Byte Club is complete!

你可能感兴趣的:(ios7)