预计阅读本页时间:-
Chapter 10
Getting Started with Web Services
WHAT YOU WILL LEARN IN THIS CHAPTER:
- Making simple networking calls
- Parsing JSON web service responses
- Streaming media from a URL using the Media Player
- Opening iTunes from within an application
WROX.COM CODE DOWNLOADS FOR THIS CHAPTER
广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元
You can find the wrox.com code downloads for this chapter at www.wrox.com/go/begiosprogramming on the Download Code tab. The code is in the Chapter 10 download and individually named according to the names throughout the chapter.
In the 1999, the phrase Web 2.0 was coined to describe the slew of new websites and services being created. Websites in the past had been static and required new files to be uploaded to servers to change the content of the site. Web 2.0 sites use dynamic content with a back-end data store to update content on the site without needing to update the files on the server. Blogging sites are a simple example, whereas sites such as Facebook and Twitter are examples of more complex services. Users can see new content on the site without having to even reload the page. These sites typically use a set of API calls made using the Hyper Text Transfer Protocol, better known as HTTP, to get new content. The set of API calls are known as web services.
Mobile apps have become an extension of these Web 2.0 sites. Using the same set of APIs, mobile apps can move away from needing new builds and releases to update their content and instead connect to these web services to get new data. This gives developers an endless amount of new data they can add to their apps.
In this chapter you use the iTunes Search web service to add the Search for Tracks feature to the Bands app.
LEARNING ABOUT WEB SERVICES
Web services enable two computers to exchange data over the web, typically using HTTP. Early web services used documented protocols that the data exchange conformed to. One of the first web service protocols was the Simple Object Access Protocol (SOAP), which was designed by Microsoft. SOAP uses the Extensible Markup Language (XML) to create messages that can be sent over HTTP. It was a popular choice of those developing software using Microsoft developer tools, because of its integration into those tools. Developers who were not using Microsoft tools found it to be overly complicated and hard to implement. As an alternative, developers began creating REST web services.
REST, which stands for Representational State Transfer, is a design pattern. Instead of creating a new protocol, REST web services build on documented aspects of HTTP. Because it’s a design pattern and not a protocol, implementations of REST web services vary greatly, though the basic concept remains the same. In a REST web service the URL is used in conjunction with the four main HTTP verbs to perform basic tasks. The GET verb is used to request and retrieve data from the service, while the DELETE verb is used to delete data. The PUT and POST verbs both send data to a web service with PUT meaning to update data, while POST creates new data.
Exploring the iTunes Search API
The iTunes Search API (www.apple.com/itunes/affiliates/resources/documentation/itunes-store-web-service-search-api.html) is a REST API that does not require a username and password, making it a great tool to learn about web services. You can use the service to search iTunes for music, videos, books, and even other apps. In the Bands app you use it to search for tracks by band.
The search uses a well-formed URL and the HTTP GET verb to ask for data. This is the basic structure of the URL:
https://itunes.apple.com/search
You then add query parameters to the end of the URL the same way you built the Yahoo search URL in Chapter 8, “Using Web Views.” In the Bands app you want to search for tracks only by artist. Table 10-1 describes the parameters and values you can use to build the search request.
PARAMETER KEY VALUE DESCRIPTION
media music Tells the service you would like to search for music only.
entity musicTrack Tells the service you would like to search for music tracks only and not music videos.
term (band name) The name of the band you would like to search for
TABLE 10.1: iTunes Search Parameters
Using these parameters and the band Rush as an example, the search URL would look like this:
https://itunes.apple.com/search?media=music&entity=musicTrack&attribute=artistTerm&term=Rush
Using Safari on your desktop or laptop, you can enter that URL into the address bar and view the results, as shown in Figure 10-1.
Discussing JSON
The results from the iTunes Search API are returned in JavaScript Object Notation, or JSON. Originally designed for communicating between a web browser and server, it has become the most popular way of sending data in web services. It’s a human-readable format that uses brackets and curly brackets to denote arrays and objects as well as key-value data with the key first, followed by a comma and then the data. Listing 10-1 shows a subset of the JSON returned by the iTunes Search API using the example URL in the previous section.
LISTING 10-1: JSON Results Sample
{
"resultCount":50,
"results":
[
{
"wrapperType":"track",
"kind":"song",
"artistId":50526,
"collectionId":643419092,
"trackId":643419201,
"artistName":"Rush",
"collectionName":"Moving Pictures (Remastered)",
"trackName":"Tom Sawyer",
"collectionCensoredName":"Moving Pictures (Remastered)",
"trackCensoredName":"Tom Sawyer",
"artistViewUrl":"https://itunes.apple.com/us/artist/rush/id50526?uo=4",
"collectionViewUrl":"https://itunes.apple.com/us/album/
tom-sawyer/id643419092?i=643419201&uo=4",
"trackViewUrl":"https://itunes.apple.com/us/album/
tom-sawyer/id643419092?i=643419201&uo=4",
"previewUrl":"http://a1005.phobos.apple.com/us/r1000/061/Music2/
v4/4b/a1/aa/4ba1aa72-a6f5-4ac3-1b66-ca747aa490f8/
mzaf_4660742303953455851.aac.m4a",
"artworkUrl30":"http://a2.mzstatic.com/us/r30/Music/
v4/17/ce/bc/17cebc97-e0cb-4774-8503-d7980e27f509/
UMG_cvrart_00602527893426_01_RGB72_1498x1498_12UMGIM19114.30x30-50.jpg",
"artworkUrl60":"http://a1.mzstatic.com/us/r30/Music/
v4/17/ce/bc/17cebc97-e0cb-4774-8503-d7980e27f509/
UMG_cvrart_00602527893426_01_RGB72_1498x1498_12UMGIM19114.60x60-50.jpg",
"artworkUrl100":"http://a3.mzstatic.com/us/r30/Music/
v4/17/ce/bc/17cebc97-e0cb-4774-8503-d7980e27f509/
UMG_cvrart_00602527893426_01_RGB72_1498x1498_12UMGIM19114.100x100-75.jpg",
"collectionPrice":9.99,
"trackPrice":1.29,
"releaseDate":"2013-05-14T07:00:00Z",
"collectionExplicitness":"notExplicit",
"trackExplicitness":"notExplicit",
"discCount":1,
"discNumber":1,
"trackCount":7,
"trackNumber":1,
"trackTimeMillis":276880,
"country":"USA",
"currency":"USD",
"primaryGenreName":"Rock",
"radioStationUrl":https://itunes.apple.com/us/station/idra.643419201
}
]
}
The first data field in this sample is the resultCount whose value is 50. The next is an array of result objects. Only one is shown in the listing, although the full result set has 50 objects in the array. The result objects themselves have more than 30 fields, depending on what type of media it is. For the Bands app you can search for tracks. Table 10-2 lists the fields in the search results the Bands app will use.
RESULT KEY DESCRIPTION
collectionName The name of the album or collection the track is part of
trackName The name of the track
trackViewUrl The URL of the track in the iTunes store
previewUrl The URL to a preview of the track provided by Apple
TABLE 10.2: iTunes Search Result Keys
Adding the Search View
To start the iTunes search feature, you first need to add a new scene to the Bands app to perform the search and display the results. In this scene you use a UISearchBar and UITable view. A UISearchBar is similar to a UITextField in the way it uses the software keyboard, which you learned about in Chapter 4, “Creating a User Input Form.” When it becomes the first responder the keyboard is shown with a button labeled “Search.” You then use the UISearchBarDelegate protocol to know when users tap the search button, as you will implement in the following Try It Out.
TRY IT OUT: Using a Search Bar
- From the Xcode menu select File ⇒ New ⇒ File and create a new subclass of the UITableViewController class named WBAiTunesSearchViewController.
- Select the WBAiTunesSearchViewController.h file from the Project Navigator.
- Declare the class implements the UISearchBarDelegate with the following code:
@interface WBAiTunesSearchViewController : UITableViewController
<UISearchBarDelegate> - Add an IBOutlet for a UISearchBar and a property for the band name using the following code:
@property (nonatomic, assign) IBOutlet UISearchBar *searchBar;
@property (nonatomic, strong) NSString *bandName; - Select the WBAiTunesSearchViewController.m file from the Project Navigator, and add the viewWillAppear: method using the following code:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
self.searchBar.text = self.bandName;
} - Add the searchBarSearchButtonClicked: method of the UISearchBarDelegate using the following code:
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar
{
NSLog(@"Search Button Tapped");
} - Select the Main.storyboard from the Project Navigator.
- Drag a new Table View Controller from the Object Library onto the Storyboard.
- Select the new Table View Controller, and set its class to the WBAiTunesSearchViewController in the Identity Inspector. This is now the iTunes Search scene.
- Create a new push segue from the Band Details scene to the iTunes Search scene, and set its identity to iTunesSearchSegue in the Attributes Inspector.
- Select the UINavigationItem in the iTunes Search scene, and set its title to iTunes Track Search.
- Drag a new Search Bar from the Object library, and add it to the top of the UITableView in the iTunes Search scene. You will know if it’s a subview of the UITableView by looking at the Storyboard hierarchy, as shown in Figure 10-2.
FIGURE 10-2
- Connect the UISearchBar to the IBOutlet in the WBAiTunesSearchViewController.
- Connect the delegate for the UISearchBar to the WBAiTunesSearchViewController.
- Select the WBABandDetailsViewController.h file and add a new value to the WBAActivityButtonIndex enumeration using the following code:
typedef enum {
// WBAActivityButtonIndexEmail,
// WBAActivityButtonIndexMessage,
WBAActivityButtonIndexShare,
WBAActivityButtonIndexWebSearch,
WBAActivityButtonIndexFindLocalRecordStores,
WBAActivityButtonIndexSearchForTracks,
} WBAActivityButtonIndex; - Select the WBABandDetailsViewController.m file from the Project Navigator.
- Add the WBAiTunesSearchViewController.h file to the imports using the following code:
#import "WBABandDetailsViewController.h"
#import <MessageUI/MFMailComposeViewController.h>
#import <MobileCoreServices/MobileCoreServices.h>
#import "WebViewController.h"
#import "WBAiTunesSearchViewController.h" - Modify the activityButtonTouched: method using the following code:
- (IBAction)activityButtonTouched:(id)sender
{
UIActionSheet *activityActionSheet = nil;
activityActionSheet = [[UIActionSheet alloc] initWithTitle:nil
delegate:self cancelButtonTitle:@"Cancel" destructiveButtonTitle:nil
otherButtonTitles:@"Share", @"Search the Web", @"Find Local Record Stores",
@"Search iTunes for Tracks", nil];
activityActionSheet.tag = WBAActionSheetTagActivity;
[activityActionSheet showInView:self.view];
} - Modify the actionSheet:clickedButtonAtIndex: method with the following code:
- (void)actionSheet:(UIActionSheet *)actionSheet
clickedButtonAtIndex:(NSInteger)buttonIndex
{
if(actionSheet.tag == WBAActionSheetTagActivity)
{
if(buttonIndex == WBAActivityButtonIndexShare)
{
[self shareBandInfo];
}
else if (buttonIndex == WBAActivityButtonIndexWebSearch)
{
[self performSegueWithIdentifier:@"webViewSegue" sender:nil];
}
else if (buttonIndex == WBAActivityButtonIndexFindLocalRecordStores)
{
[self performSegueWithIdentifier:@"mapViewSegue" sender:nil];
}
else if (buttonIndex == WBAActivityButtonIndexSearchForTracks)
{
[self performSegueWithIdentifier:@"iTunesSearchSegue" sender:nil];
}
}
// the rest of this method is available in the sample code
} - Modify the prepareForSegue:sender: method with the following code:
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if([segue.destinationViewController class] == [WebViewController class])
{
WebViewController *webViewController = segue.destinationViewController;
webViewController.bandName = self.bandObject.name;
}
else if ([segue.destinationViewController class] ==
[WBAiTunesSearchViewController class])
{
WBAiTunesSearchViewController *WBAiTunesSearchViewController =
segue.destinationViewController;
WBAiTunesSearchViewController.bandName = self.bandObject.name;
}
} - Run the app in the iPhone 4-inch simulator. When you select the Search iTunes for Tracks option activity, you now see the iTunes Search scene with the UISearchBar containing the band name, as shown in Figure 10-3.
FIGURE 10-3
How It Works
The first thing you did was to create a new subclass of UITableViewController called WBAiTunesSearchViewController. In its interface, you declared that it implements the UISearchBarDelegate protocol and added an IBOutlet to a UISearchBar as well as a property for the band name to search for. In the implementation you added the viewDidAppear: method to set the text property of the UISearchBar using the bandName property.
In the Main.storyboard you added a new table view controller and set its class to the new WBAiTunesSearchViewController. This is now the iTunes Search scene. Because its parent class is UITableViewController, the delegate and dataSource of the UITableView and the tableView property are automatically connected.
Next, you created a manual push segue to the iTunes Search scene from the Band Details scene. With the segue added, the iTunes Search scene gets a UINavigationItem whose title you set to iTunes Track Search. You then added a new UISearchBar to the scene, making sure to add it to the UITableView. This allows the UISearchBar to scroll off screen while the user looks at search results. Finally, you connected the UISearchBar and its delegate to the WBAiTunesSearchViewController.
In the WBABandDetailsViewController implementation, you added one more option to the UIActionSheet, as you have done in previous chapters. You also updated the prepareForSegue:sender: method to set the bandName property of the WBAiTunesSearchViewController before the segue is performed. When selected this new option segues to the new iTunes Search scene.
INTRODUCING NSURLSESSION
The iOS SDK has a rich set of networking classes and protocols. Developers can control everything from caching and authentication to processing data as it is streamed in. This is great for applications that need that level of detail in their networking code, but it adds a lot of complexity for apps that need to make only simple network calls.
To address some of these complexities, Apple added the NSURLSession class and its companion classes and delegates to iOS 7. Using these classes developers can create different tasks that the system then handles, executing instead of having to implement all the lower-level details.
You can create three basic types of tasks. These tasks can then use either delegates to get the response and data or they can use completion handlers, as you did in the previous chapter.
- Data task: A data task is a simple http GET call that downloads data into memory.
- Download task: A download task is similar to a data task, except that the data is saved to a file on disk.
- Upload task: The third is an upload task that uploads a file from disk.
NOTE For apps that require authentication, you need to implement the delegates that handle authentication challenges. This is beyond the scope of this book. To learn more about NSURLSession and its various delegates, refer to the URL Loading System Programming guide provided by Apple at https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/URLLoadingSystem/URLLoadingSystem.html#//apple_ref/doc/uid/10000165i.
Creating and Scheduling a Data Task
In the Bands app, the app needs to make a GET request then process the data that is returned. You will use an NSURLSessionDataTask to do this. The iTunes search API does not require a username and password, so there is no need to add any authentication capabilities. This means that you do not need to implement any delegates to handle authentication challenges. It also means that you can use the shared NSURLSession that uses the system defaults for its configuration. Because the results returned from the iTunes search API are relatively small, you also do not need to handle data as it’s streamed from the network connection. Instead you can use a completion handler to process the data after it has been completely downloaded.
In Chapter 9 the documentation for MKLocalSearch explicitly stated the completion handler code would be executed on the main thread. This is not the case for NSURLSession. Since the code in the completion handler will be updating the user interface, you will need to code it in a way that ensures it is executed on the main thread. There are a handful of ways to do this. In the Bands app you will use Grand Central Dispatch.
Grand Central Dispatch, or GCD, was created by Apple and included in iOS 4. It was designed to remove much of the complexity of threading. The implementation of GCD still uses threads, but you as the developer no longer need to worry about them. Instead you dispatch blocks of code to different queues, which then schedule them to run on threads the system maintains. You use the dispatch_async function to do this. This function takes two parameters. The first is the system queue to perform the block on and the second is the block itself. To get the main queue you use the dispatch_get_main_queue function, which takes no parameters. Listing 10-2 shows a simple example of this syntax.
LISTING 10-2: Using dispatch_async
dispatch_async(dispatch_get_main_queue(),
^{
NSLog(@"This will be scheduled and executed on the main thread");
});
The NSURLSession method you will use to call the iTunes search API is the dataTaskWithRequest:completionHandler: method. The request you pass in is an NSURLRequest, which you learned about in Chapter 8. You will build this request in the same manner as the Yahoo search request.
The block for the completion handler gets three parameters passed to it. The first is an NSData object that holds the data being returned from the request. The second is an NSURLResponse object, which holds the HTTP response. The last is an NSError object that will be set if the system encounters an error while performing the task. In the following Try It Out you will implement the call to the iTunes search API using the dataTaskWithRequest:completionHandler: method and print the response in the completion handler, using Grand Central Dispatch to make sure it executes on the main thread.
TRY IT OUT: Calling the iTunes Search API
- Select the WBAiTunesSearchViewController.h file from the Project Navigator.
- Declare the following method in the interface:
- (void)searchForTracks;
- Select the WBAiTunesSearchViewController.m file from the Project Navigator.
- Add the searchForTracks method using the following code:
- (void)searchForTracks
{
[self.searchBar resignFirstResponder];
NSString *bandName = self.searchBar.text;
NSString *urlEncodedBandName = (NSString *)
CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes(
NULL,(CFStringRef)bandName, NULL, (CFStringRef)@"!*'();:@&=+$,/?%#[]",
kCFStringEncodingUTF8 ));
NSString *iTunesSearchUrlString = [NSString
stringWithFormat:@"https://itunes.apple.com/
search?media=music&entity=musicTrack&term=%@", urlEncodedBandName];
NSURL *iTunesSearchUrl = [NSURL URLWithString:iTunesSearchUrlString];
NSURLRequest *iTunesSearchUrlRequest = [NSURLRequest
requestWithURL:iTunesSearchUrl];
NSURLSession *sharedUrlSession = [NSURLSession sharedSession];
NSURLSessionDataTask *searchiTunesTask =
[sharedUrlSession dataTaskWithRequest:iTunesSearchUrlRequest completionHandler:
^(NSData *data, NSURLResponse *response, NSError *error)
{
dispatch_async(dispatch_get_main_queue(),
^{
[UIApplication sharedApplication].networkActivityIndicatorVisible
= NO;
if(error)
{
UIAlertView *searchAlertView = [[UIAlertView alloc]
initWithTitle:@"Error" message:error.localizedDescription delegate:nil
cancelButtonTitle:@"OK" otherButtonTitles:nil];
[searchAlertView show];
}
else
{
NSString *resultString = [[NSString alloc] initWithData:data
encoding:NSUTF8StringEncoding];
NSLog(@"Search results: %@", resultString);
}
});
}];
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
[searchiTunesTask resume];
} - Modify the viewWillAppear method with the following code:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
self.searchBar.text = self.bandName;
[self searchForTracks];
} - Modify the searchBarSearchButtonClicked: method of the UISearchBarDelegate using the following code:
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar
{
[self searchForTracks];
} - Run the app in the iPhone 4-inch simulator. When you select the Search iTunes for Tracks option, you see information about the search in the Xcode debug console, as shown in Figure 10-4.
FIGURE 10-4
How It Works
In the interface of the WBAiTunesSearchViewController, you declared a new method called searchForTracks. In its implementation the first thing the code does is resign the UISearchBar as the first responder. This hides the keyboard if it is visible. Next, it gets the band name from the UISearchBar and URL encodes it using the same Core Foundation method you used in Chapter 8 to create the Yahoo search request. The code then builds the iTunes search URL string using the parameters discussed earlier in this chapter. Creating the NSURL and NSURLRequest are also the same as you implemented in Chapter 8.
Before creating the networking task, the code first gets the shared NSURLSession using the static sharedSession method of the NSURLSession class. You then use this instance to create a new NSURLSessionDataTask using the NSURLRequest and passing in a completion handler block.
The completion handler uses Grand Central Dispatch to make sure its code is executed on the main thread. It then hides the Network Activity Indicator and checks for any errors that may have occurred. If there is an error, the user is alerted; otherwise, the NSData returned from the data task is converted to an NSString using the initWithData:encoding: method and written to the debug console.
Creating the NSURLSessionDataTask does not start the request like the MKLocalSearch did. Instead you initiate the network request by calling the resume method of the NSURLSessionDataTask. The code does this after making the Network Activity Indicator visible.
Parsing JSON
A big advantage JSON has over XML in iOS is that it already conforms to data structures in Objective-C. Because all data in JSON is key-value formatted, NSDictionary is a natural match for mapping the data to Objective-C. The JSON returned from the iTunes Search API can be mapped to an NSDictionary with two keys: resultCount and results. The object stored for the resultCount key is an NSNumber with a value of 50. The object stored for the results key is an NSArray which contains NSDictionary objects that represent each track in the search results.
Parsing the actual data returned from the service may seem complicated, but fortunately Apple has already created a parser for you with the NSJSONSerialization class. It has a static method called JSONObjectWithData:options:error:, which returns the NSDictionary representing the JSON. The options tell the parser if you would like the data structures to be mutable. Since the Bands app won’t be modifying the search results, you can pass 0 for the options. The method also takes an NSError parameter. It gets passed by reference which, as you learned in Chapter 2, “Introduction to Objective-C,” means you are passing the address for an NSError object and not the object itself. If an error occurs while parsing, the parser will create an actual NSError object and set the address you passed in to point to it. The following Try It Out shows you how to use the NSJSONSerialization class to parse the results for the iTunes search.
TRY IT OUT: Parsing iTunes Search Results
- Select the WBAiTunesSearchViewController.m file from the Project Navigator.
- Modify the completion handler for the NSURLSessionDataTask with the following code:
NSURLSessionDataTask *searchiTunesTask =
[sharedUrlSession dataTaskWithRequest:iTunesSearchUrlRequest completionHandler:
^(NSData *data, NSURLResponse *response, NSError *error)
{
dispatch_async(dispatch_get_main_queue(),
^{
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
if(error)
{
UIAlertView *searchAlertView = [[UIAlertView alloc]
initWithTitle:@"Error" message:error.localizedDescription delegate:nil
cancelButtonTitle:@"OK" otherButtonTitles:nil];
[searchAlertView show];
}
else
{
NSString *resultString = [[NSString alloc] initWithData:data
encoding:NSUTF8StringEncoding];
NSLog(@"Search results: %@", resultString);
NSError *jsonParseError = nil;
NSDictionary *jsonDictionary = [NSJSONSerialization
JSONObjectWithData:data options:0 error:&jsonParseError];
if(jsonParseError)
{
UIAlertView *jsonParseErrorAlert = [[UIAlertView alloc]
initWithTitle:@"Error" message:jsonParseError.localizedDescription delegate:nil
cancelButtonTitle:@"OK" otherButtonTitles:nil];
[jsonParseErrorAlert show];
}
else
{
for(NSString *key in jsonDictionary.keyEnumerator)
{
NSLog(@"First level key: %@", key);
}
}
}
});
}]; - Run the app in the iPhone 4-inch simulator. When searching for tracks you now see more information in the Xcode console, as shown in Figure 10-5.
FIGURE 10-5
How It Works
If the data task returns successfully, the code now attempts to parse the data using the NSJSONSerialization class. The code first creates a pointer to an NSError and sets its value to nil, meaning it doesn’t point to anything. It then passes the data from the NSURLSessionDataTask, 0 for the options (meaning it’s OK to use immutable data structures) and the NSError pointer to the JSONObjectWithData:options:error: method. When the parse completes, the code checks to see if the NSError pointer was set to an actual NSError object. If so, an error has occurred during the parsing and the user is alerted. Otherwise, it prints the first-level keys of the dictionary—resultCount and results—to the debug console.
DISPLAYING SEARCH RESULTS
The last part of the iTunes Track Search feature is to display all the tracks in alphabetical order then give the user the ability to preview the track or to view the track in iTunes. The data source for the UITableView uses the same approach you implemented to display the bands. First, you need to create an object to hold the properties of each track returned from the search. You can then create the same first letters NSMutableArray to create the table index and a tracks NSMutableDictionary to hold the track objects for each of the results.
TRY IT OUT: Creating the Data Source
- Select the Main.storyboard from the Project Navigator.
- Select the prototype cell in the iTunes Search view.
- Set its style to subtitle in the Attributes Inspector.
- Set its reuse identifier to trackCell in the Attributes Inspector.
- From the Xcode menu select File ⇒ New ⇒ File, and create a new subclass of NSObject called WBATrack.
- Select the WBATrack.h file, and add properties for the track name, collection name, track preview URL, and iTunes URL using the following code:
@interface WBATrack : NSObject
@property (nonatomic, strong) NSString *trackName;
@property (nonatomic, strong) NSString *collectionName;
@property (nonatomic, strong) NSString *previewUrlString;
@property (nonatomic, strong) NSString *iTunesUrlString;
@end - Select the WBATrack.m file, and add the compare method using the following code:
- (NSComparisonResult)compare:(WBATrack *)otherObject
{
return [self.trackName compare:otherObject.trackName];
} - Select the WBAiTunesSearchViewController.h file from the Project Manager.
- Add properties for the tracks NSMutableDictionary and first letters NSMutableArray using the following code:
@property (nonatomic, strong) NSMutableArray *firstLettersArray;
@property (nonatomic, strong) NSMutableDictionary *tracksDictionary; - Select the WBAiTunesSearchViewController.m file from the Project Manager.
- Import the WBATrack.h class using the following code:
#import "WBAiTunesSearchViewController.h"
#import "WBATrack.h" - Modify the viewDidLoad method with the following code:
- (void)viewDidLoad
{
[super viewDidLoad];
self.firstLettersArray = [NSMutableArray array];
self.tracksDictionary = [NSMutableDictionary dictionary];
} - In the completion handler for the NSURLSessionDataTask add the following code to the else statement after the JSON has been successfully parsed:
for(NSString *key in jsonDictionary.keyEnumerator)
{
NSLog(@"First level key: %@", key);
}
[self.firstLettersArray removeAllObjects];
[self.tracksDictionary removeAllObjects];
NSArray *searchResultsArray = [jsonDictionary objectForKey:@"results"];
for(NSDictionary *trackInfoDictionary in searchResultsArray)
{
WBATrack *track = [[WBATrack alloc] init];
track.trackName = [trackInfoDictionary objectForKey:@"trackName"];
track.collectionName = [trackInfoDictionary objectForKey:@"collectionName"];
track.previewUrlString = [trackInfoDictionary objectForKey:@"previewUrl"];
track.iTunesUrlString = [trackInfoDictionary objectForKey:@"trackViewUrl"];
NSString *trackFirstLetter = [track.trackName substringToIndex:1];
NSMutableArray *tracksWithFirstLetter = [self.tracksDictionary
objectForKey:trackFirstLetter];
if(!tracksWithFirstLetter)
{
tracksWithFirstLetter = [NSMutableArray array];
[self.firstLettersArray addObject:trackFirstLetter];
}
[tracksWithFirstLetter addObject:track];
[tracksWithFirstLetter sortUsingSelector:@selector(compare:)];
[self.tracksDictionary setObject:tracksWithFirstLetter forKey:trackFirstLetter];
}
[self.firstLettersArray sortUsingSelector:@selector(compare:)];
[self.tableView reloadData]; - Modify the numberOfSectionsInTableView: method with the following code:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return self.firstLettersArray.count;
} - Modify the tableView:numberOfRowsInSection: method with the following code:
- (NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section
{
NSString *firstLetter = [self.firstLettersArray objectAtIndex:section];
NSArray *tracksWithFirstLetter =
[self.tracksDictionary objectForKey:firstLetter];
return tracksWithFirstLetter.count;
} - Modify the tableView:cellForRowAtIndexPath: method with the following code:
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath)indexPath
{
static NSString *CellIdentifier = @"trackCell";
UITableViewCell *cell =
[tableView dequeueReusableCellWithIdentifier:CellIdentifier
forIndexPath:indexPath];
NSString *firstLetter = [self.firstLettersArray objectAtIndex:indexPath.section];
NSArray *tracksWithFirstLetter = [self.tracksDictionary objectForKey:firstLetter];
WBATrack *track = [tracksWithFirstLetter objectAtIndex:indexPath.row];
cell.textLabel.text = track.trackName;
cell.detailTextLabel.text = track.collectionName;
return cell;
} - Add the tableView:titleForHeaderInSection: method using the following code:
- (NSString *)tableView:(UITableView *)tableView
titleForHeaderInSection:(NSInteger)section
{
return [self.firstLettersArray objectAtIndex:section];
} - Add the sectionIndexTitlesForTableView: method using the following code:
- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView
{
return self.firstLettersArray;
} - Add the tableView:sectionForSectionIndexTitle:atIndex method using the following code:
- (int)tableView:(UITableView *)tableView sectionForSectionIndexTitle:
(NSString *)title atIndex:(NSInteger)index
{
return [self.firstLettersArray indexOfObject:title];
} - Run the app in the iPhone 4-inch simulator. Searching for tracks now shows results in the Table view, as shown in Figure 10-6.
FIGURE 10-6
How It Works
The first thing you did was change the type of the prototype cell in the Storyboard to the subtitle type. This way you can display both the name of the track and the album or collection it is on in the same cell. You also set the reuse identifier of the cell.
Next, you created a new WBATrack object with four properties for the track name, collection name, iTunes URL string, and preview URL string. To sort the tracks by name, you also override the compare: method to use the track names as the comparison property.
In the WBAiTunesSearchViewController interface you declared the firstLettersArray and the tracksDictionary. These are analogous to firstLettersArray and bandsDictionary you implemented in Chapter 5, “Using Table Views.” In the implementation you initialize the array and dictionary when the view appears.
The main focus of the Try It Out is processing the results of the search and creating the data source for the UITableView. Once the JSON result has been successfully parsed, you first clear all the objects in the firstLettersArray and the tracksDictionary. This is so users do not see mixed results if they type their own search term into the UISearchBar. Next you get the NSArray of search results from the jsonDictionary using the results key. This NSArray contains NSDictionary objects for each track returned from the search. The keys for a track NSDictionary correspond to the keys in the JSON response (refer to Table 10-2). The code uses a for loop to iterate through each track NSDictionary, creating WBATrack objects for each and then using them to repopulate the firstLettersArray and the tracksDictionary.
The rest of the Try It Out implements the UITableViewDataSource protocol methods. The code is the same pattern you implemented in Chapter 5 for the bands UITableView using the firstLettersArray as an index into the tracksDictionary. The only notable difference is in the tableView:cellForRowAtIndexPath: method. Because you set the type of the prototype cell to subtitle, the detailsTextLabel will be visible in the cell. You set its text property to the collectionName property of the WBATrack.
Previewing Tracks
The iTunes Search API includes a preview URL in the results. This URL points to a media file with a preview of the track that can be streamed using the MPMoviePlayerViewController. This is a special view controller similar to the UIImagePickerController and MFMailComposeViewController you have implemented in previous chapters. It does require the MediaPlayer.framework to be added to the project. You can then import the MediaPlayer.h file to access the MPMoviePlayerViewController and the presentMoviePlayerViewControllerAnimated: method it adds to the UIViewController class. When created and presented, the movie player handles all the network connections to stream the track preview from iTunes.
To give the user the option to preview a track, your code needs to know when the user has selected a track in the UITableView. You can use the tableView:didSelectRowAtIndexPath: method of the UITableViewDelegate protocol for this. When the user selects a track, this method is called. You can then show the track options using a UIActionSheet, as you will implement in the following Try It Out.
TRY IT OUT: Using the Media Player
- Select the Project from the Project Manager, and add the MediaPlayer.framework to the linked libraries and frameworks.
- Select the WBAiTunesSearchViewController.h file.
- Declare that the class implements the UIActionSheetDelegate using the following code:
@interface WBAiTunesSearchViewController : UITableViewController
<UISearchBarDelegate, UIActionSheetDelegate> - Create a new enumeration named WBATrackOptionButtonIndex using the following code:
typedef enum {
WBATrackOptionButtonIndexPreview,
} WBATrackOptionButtonIndex; - Select the WBAiTunesSearchViewController.m file from the Project Manager.
- Add the MediaPlayer.h file to the imports using the following code:
#import "WBAiTunesSearchViewController.h"
#import "WBATrack.h"
#import <MediaPlayer/MediaPlayer.h> - Add the tableView:didSelectRowAtIndexPath: method to the implementation using the following code:
- (void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
UIActionSheet *trackActionSheet = [[UIActionSheet alloc] initWithTitle:nil
delegate:self cancelButtonTitle:@"Cancel" destructiveButtonTitle:nil
otherButtonTitles:@"Preview Track", nil];
[trackActionSheet showInView:self.view];
} - Add the actionSheet:clickedButtonAtIndex: method using the following code:
- (void)actionSheet:(UIActionSheet *)actionSheet
clickedButtonAtIndex:(NSInteger)buttonIndex
{
NSIndexPath *selectedIndexPath = self.tableView.indexPathForSelectedRow;
NSString *trackFirstLetter = [self.firstLettersArray
objectAtIndex:selectedIndexPath.section];
NSArray *tracksWithFirstLetter = [self.tracksDictionary
objectForKey:trackFirstLetter];
WBATrack *trackObject = [tracksWithFirstLetter
objectAtIndex:selectedIndexPath.row];
if(buttonIndex == WBATrackOptionButtonIndexPreview)
{
NSURL *trackPreviewURL = [NSURL URLWithString:trackObject.previewUrlString];
MPMoviePlayerViewController *moviePlayerViewController =
[[MPMoviePlayerViewController alloc] initWithContentURL:trackPreviewURL];
[self presentMoviePlayerViewControllerAnimated:moviePlayerViewController];
}
else if (buttonIndex == WBATrackOptionButtonIndexOpenIniTunes)
{
NSURL *iTunesURL = [NSURL URLWithString:trackObject.iTunesUrlString];
[[UIApplication sharedApplication] openURL:iTunesURL];
}
} - Run the app in the iPhone 4-inch simulator. Selecting a search result now previews the track in the application using the Media Player, as shown in Figure 10-7.
FIGURE 10-7
How It Works
The first thing you did was to add the MediaPlayer.framework to the project. Next you declare that the WBAiTunesSearchViewController implements the UIActionSheetDelegate. You also added a new enumeration named WBATrackOptionButtonIndex, which maps to the options that will be shown in the UIActionSheet.
In the implementation you added the MediaPlayer.h file to the imports so you can access the MPMoviePlayerViewController. You then added the tableView:didSelectRowAtIndexPath: method that creates a new UIActionSheet with Preview Track as the only option.
Finally, you added the actionSheet:didClickButtonAtIndex: method. It first gets the WBATrack object correlating to the NSIndexPath of the currently selected row in the UITableView. If the preview button is clicked, it creates a new NSURL using the trackPreviewUrl property of the WBATrack object. It then creates a new instance of the MPMoviePlayerViewController using the initWithContentURL: method and the NSURL. The MPMoviePlayerViewController is then presented using the presentMoviePlayerViewControllerAnimated method.
Showing Tracks in iTunes
The last part of the Search iTunes for Tracks feature is opening iTunes to the track so that the user can purchase it. You do this the same way you opened Safari in Chapter 8. The system knows to pass URLs pointing to http://itunes.apple.com to the iTunes app, so all you need to do in code is call the openURL method of the shared application.
TRY IT OUT: Opening iTunes
- Select the WBAiTunesSearchViewController.h file and add another value to the WBATrackOptionButtonIndex enumeration using the following code:
typedef enum {
WBATrackOptionButtonIndexPreview,
WBATrackOptionButtonIndexOpenIniTunes,
} WBATrackOptionButtonIndex; - Select the WBAiTunesSearchViewController.m file from the Project Navigator.
- Modify the tableView:didSelectRowAtIndexPath: method with the following code:
- (void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
UIActionSheet *trackActionSheet = [[UIActionSheet alloc] initWithTitle:nil
delegate:self cancelButtonTitle:@"Cancel" destructiveButtonTitle:nil
otherButtonTitles:@"Preview Track", @"Open in iTunes", nil];
[trackActionSheet showInView:self.view];
} - Modify the actionSheet:clickedButtonAtIndex: method with the following code:
- (void)actionSheet:(UIActionSheet *)actionSheet
clickedButtonAtIndex:(NSInteger)buttonIndex
{
NSIndexPath *selectedIndexPath = self.tableView.indexPathForSelectedRow;
NSString *trackFirstLetter = [self.firstLettersArray
objectAtIndex:selectedIndexPath.section];
NSArray *tracksWithFirstLetter = [self.tracksDictionary
objectForKey:trackFirstLetter];
WBATrack *trackObject = [tracksWithFirstLetter
objectAtIndex:selectedIndexPath.row];
if(buttonIndex == WBATrackOptionButtonIndexPreview)
{
NSURL *trackPreviewURL = [NSURL URLWithString:trackObject.previewUrlString];
MPMoviePlayerViewController *moviePlayerViewController =
[[MPMoviePlayerViewController alloc] initWithContentURL:trackPreviewURL];
[self presentMoviePlayerViewControllerAnimated:moviePlayerViewController];
}
else if (buttonIndex == WBATrackOptionButtonIndexOpenIniTunes)
{
NSURL *iTunesURL = [NSURL URLWithString:trackObject.iTunesUrlString];
[[UIApplication sharedApplication] openURL:iTunesURL];
}
} - Run the app on an iOS test device. Selecting Open in iTunes now opens iTunes and scrolls to the track.
How It Works
First, you added the WBATrackOptionButtonIndexOpenIniTunes value to the WBATrackOptionButtonIndex enumeration, which maps to the option in the UIActionSheet. Next, you added the option to the UIActionSheet that is displayed when the user selects a track. Finally, in the actionSheet:clickedButtonAtIndex: method you created a new NSURL using the iTunesUrlString property of the WBATrack object and passed it into the openURL method of the shared UIApplication.
NOTE The iOS simulator does not include the iTunes app. If you try the Open in iTunes option for a track in the simulator, it will attempt to open the URL in Safari. This results in a Cannot Open Page error. This is not an error with your code, but a limitation of the iOS Simulator. To test the Open in iTunes option you need to use a physical device.
SUMMARY
Web services add a whole new dynamic to mobile apps. They can be used to build all kinds of new and interesting features for your users. To lower the barrier for developers, Apple has put a lot of effort into its networking classes and protocols to make simple tasks easy while still providing the flexibility for developers to get to the lower-level details. In this chapter you implemented the Search iTunes for Tracks feature of the Bands app. It uses an NSURLSessionDataTask to query the iTunes Search API. The results are passed back using a completion handler that parses the JSON response and displays the results in a UITableView. When users select a track, they are given the option to either preview the track using the MPMoviePlayerViewController or view them in iTunes.
EXERCISES
- What are the three types of NSURLSession tasks?
- What is the name of the technology Apple introduced in iOS 4 to reduce the complexity of threading for developers?
- What class and method can you use to parse JSON into Objective-C objects?
- What framework is required to use the MPMoviePlayerViewController?
WHAT YOU LEARNED IN THIS CHAPTER
TOPIC KEY CONCEPTS
Web Services Using applications that connect to web services to add dynamic features
Networking Making network connections using NSURLSession to reduce the complexity of an application so that simple tasks can be performed with just a few lines of code
JSON Parsing Using JSON, the most popular format for passing data between web services, which structure maps nicely with Objective-C data objects, making it easy to work within iOS applications
iTunes Search API Using the iTunes Search API to query for different media types on sale in the iTunes store, as well as providing ways to preview them or open them in iTunes for purchase