This chapter covers how to use web content with Objective-C.
The recipes in this chapter will show you how to:
NSURLConnection
to asynchronously consume web contentYou want to download a file from the Internet.
Use NSURL
to specify a URL for a file and then use NSData
to download the contents of that file into your file system.
NOTE: URL stands for Uniform Resource Locator. A URL is a character string that specifies the location of an Internet resource. NSURL
is a Foundation class that lets you use URLs in Objective-C.
For this solution you must have a file available on the Internet that you can download. I posted a text file to my blog to use in this example. The URL of that file is:
http://howtomakeiphoneapps.com/wp-content/uploads/2012/03/objective-c-recipes-
example-file.txt
The first part of this process requires you to create a new NSURL
object with the URL of the resource that you want to download. Using the URL just provided, it looks like this:
NSURL *remoteTextFileURL = [NSURL
URLWithString:@"http://howtomakeiphoneapps.com/wp-
content/uploads/2012/03/objective-c-recipes-example-file.txt"];
Next, create a new NSData
object with the contents of the NSURL
object.
NSData *remoteTextFileData = [NSData dataWithContentsOfURL:remoteTextFileURL];
You can use the NSData
object right in your Objective-C program (see Recipe 4.11 for examples of working with NSData
) or save it to the file system like this:
[remoteTextFileData writeToFile:@"/Users/Shared/objective-c-recipes-example-file.txt"
atomically:YES];
See Listing 7-1 for the code.
#import <Foundation/Foundation.h>
int main (int argc, const char * argv[]){
@autoreleasepool {
NSURL *remoteTextFileURL = [NSURL URLWithString:
@"http://howtomakeiphoneapps.com/wp-content/uploads/2012/03/objective-c-recipes-
example-file.txt"];
NSData *remoteTextFileData = [NSData dataWithContentsOfURL:remoteTextFileURL];
[remoteTextFileData writeToFile:@"/Users/Shared/objective-c-recipes-
example-file.txt"
atomically:YES];
}
return 0;
}
To try this recipe out, set up a Mac command-line Xcode project and change the code in your main.m
file to look like the code in Listing 7-1. Build and run the command-line app to download the file into this location on your Mac:
/Users/Shared/objective-c-recipes-example-file.txt
Locate and open this file to see if the download was successful.
You would like to add web services that use XML data to your application.
NOTE: Internet companies publish web services to allow developers to include their services in the developer’s applications. Web services work like a web browser. In a web browser, you type in a web address (the request), hit return, and wait for a response from a remote computer on the Internet. When that response comes back, the web browser uses the rules and content in the response to present a web page to you. Web services work the same way except that the application sends the request and gets the response.
Internet companies do their best to formulate web service requests and responses using standard formats that make it easier for the applications to use their services. Web requests are strings of characters (like a web address) while web responses are strings of characters formatted as XML or JSON. XML and JSON will be discussed in full later.
Formulate a request string based on the documentation that the publisher of the web service provides. Create an NSURL
object based on the request string and NSData
to download the response from the web service. Use NSXMLParser
to go through the XML document that you get back.
For this recipe, you are going to learn how to consume a web service that is provided by a company called bitly. This company publishes a web service that you can use to shorten a long URL. All you have to do is send a request to the bitly web service with the long URL along with your bitly credentials in the format that they expect; bitly sends you an XML file with the shortened URL included in the contents.
NOTE: To follow along with this recipe, you need to create a (free) account with bitly and get your own API key and API username. Go to https://bit.ly
to get your account.
I’m going to work through this recipe with a command line-based Mac application in Xcode, but you can follow with any project type that you like. Since NSXMLParser
uses the Delegation design pattern, you need to locate your code in a class that can adopt a protocol and otherwise support Delegation. Add a new class to your project by going to File New File Objective-C class. Name the class LinkShortener
.
The interface for LinkShortener
needs to include a forward declaration for an NSMutableString
named recorderString
that will record the data you get from the web service. LinkShortener
also needs a string for keeping track of the area in the XML file where the XML parser is currently looking. Call that variable currentElement
and make both currentElement
and recorderString
private. Also, you need a forward declaration for the function that you call when you want this object to shorten a URL; call this function getTheShortUrlVersionOfThisLongURL
. The interface for LinkShortener
should look like this:
#import <Foundation/Foundation.h>
@interface LinkShortener : NSObject{
@private
NSMutableString *recorderString;
NSString *currentElement;
}
-(NSString *)getTheShortURLVersionOfThisLongURL:(NSString *)longURL;
@end
Before we move on, let’s discuss what XML is and how NSXMLParser
reads XML documents. XML stands for EXtensible Markup Language and it’s used to store and transport data. XML works by enclosing data with opening and closing tags. Opening tags are characters surrounded by the characters <
and >
. Closing tags are characters surrounded by the characters </
and >
. The tags and data together are referred to as an element.
For example, if I had an XML data type for a person, I might use an opening tag like <Person>
. The closing tag would look like </Person>
. The characters in the middle are the data. The whole thing together looks like this:
<Person>Matthew J. Campbell</Person>
XML tags are intended to be descriptive so it’s obvious what the tags mean. An entire document will have many tags with data and can be arranged in a hierarchy. So you may have tagged data within other tagged data, like this:
<Person>
<Name>Matthew J. Campbell</Name>
<Gender>Male</Gender>
</Person>
NSXMLParser
reads through an XML document starting from the beginning, reading element by element until it reaches the end. If NSXMLParser
was reading the XML above, it would start with the Person
element, and then move on to the Name
element, and then the Gender
element. This method of parsing XML is called Simple API for XML (SAX).
You’re going to use delegation to parse the XML data that you get from bitly. As the parser looks at each element in the document, it sends a message to the delegate LinkShortener
object, giving you a chance to extract the data from each element.
So LinkShortener
needs to be able to act as a delegate for NSXMLParser
and this means that you need LinkShortener
to adopt the NSXMLParserDelegate
protocol. Do this by including the protocol name right after the NSObject
superclass.
#import <Foundation/Foundation.h>
@interface LinkShortener : NSObject<NSXMLParserDelegate>{
@private
NSMutableString *recorderString;
NSString *currentElement;
}
-(NSString *)getTheShortURLVersionOfThisLongURL:(NSString *)longURL;
@end
Now that you have adopted this protocol, you need to implement at least these two delegate methods: parser:didStartElement:namespaceURI:qualifiedName:attributes:
and parser:foundCharacters:
.
Since your application only needs data from one element in the XML file, you can get away with only these two delegate methods. But, if necessary, you can always implement parser:didEndElement:namespaceURI:qualifiedName:
if you want to be notified when the parser encounters a closing tag in the XML data.
Open LinkShortener.m
to implement the first delegate method.
- (void) parser:(NSXMLParser *)parser
didStartElement:(NSString *)elementName
namespaceURI:(NSString *)namespaceURI
qualifiedName:(NSString *)qName
attributes:(NSDictionary *)attributeDict{
currentElement = [elementName copy];
if ([elementName isEqualToString:@"shortUrl"]) {
recorderString = [[NSMutableString alloc] init];
}
}
Remember that this delegate method executes each time a new XML element is reached (most XML files have lots of elements in them). For this reason, make a copy of the parameter elementName
and put it into currentElement
. You want to be able to keep track of what element you are in when the other delegate methods execute.
The other significant part of this code is the if
statement where you test to see if you are in the element that corresponds to shortUrl
. This is the element where bitly puts the shortened URL string. If you do encounter the shortULR
element, you will create and initialize a new NSMutableString
to be used later to record what is found in the element.
Now you can implement the next delegate method. This delegate method executes each time characters are encountered in an XML element in the file. You can test to see if the XML parser is in the shortUrl
element. If the answer is yes, append the characters that are found to the recorderString NSMutableString
.
- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string{
if ([currentElement isEqualToString:@"shortUrl"])
[recorderString appendString:string];
}
All of this prepares LinkShortener
to receive the XML data. Now you can prepare the request and send that off to the web server to download the response. All of this takes place in the function getTheShortUrlVersionOfThisLongURL:
, which you can start coding in the LinkShortener
implementation file.
-(NSString *)getTheShortUrlVersionOfThisLongURL:(NSString *)longURL{
}
The first thing you want to do in this function is to compose the request string. To do this, you compose a string based on the request string that bitly requires along with the longURL
parameter and your own API key and API login.
#warning Get your API Login from https://bitly.com/a/your_api_key and put it here before
running
NSString *APILogin = @"[YOUR API LOGIN]";
#warning Get your API key from https://bitly.com/a/your_api_key and put it here before
running
NSString *APIKey = @"[YOUR API KEY]";
NSString *requestString = [[NSString alloc] initWithFormat:
@"http://api.bit.ly/shorten?version=2.0.1&longUrl=%@&login=%@&apiKey=%@&format=xml",
longURL, APILogin, APIKey];
I’ve included warnings in the example code so that you remember to include your own credentials here. Also, if you look closely at the first part of the request string you’ll see that there is a format parameter with the value of xml being returned, (format=xml
). This is how you tell bitly that you want the response to come back as XML.
Next, you need an NSURL
object, which you can make based on the request string.
NSURL *requestURL = [NSURL URLWithString:requestString];
To download the data, use NSData
as you did in Recipe 7.1.
Here is a good place to set recorderString
to nil just in case this function has been used before in this object’s lifetime. Now that you’ve downloaded the data, you may use NSXMLParser
to go through the XML and pick out the content included in the shortUrl
element.
To do this, instantiate a new NSXMLParser
with the downloaded NSData object, set this object’s delegate to self
, and then send the parse message to the NSXMLParser
object.
NSXMLParser *xmlParser = [[NSXMLParser alloc] initWithData:responseData];
xmlParser.delegate = self;
[xmlParser parse];
At this point, the XML parser looks through the data and uses the delegate methods previously coded to pick out the meaningful content. Everything that the XML parser finds is recorded in recorderString
. Once the XML parser is finished, you can return the results back to the caller.
if(recorderString)
return [recorderString copy];
else
return nil;
You can use an if
statement here to send a copy of recorderString
if any data was found.
Finally, to use the function from another part of your program, you need to import the LinkShortener
header file, instantiate a LinkShortener
object, and then use the function with a long URL. Here is how to do this from main.m
:
#import <Foundation/Foundation.h>
#import "LinkShortener.h"
int main(int argc, const char * argv[]){
@autoreleasepool {
NSString *longURL = @"http://howtomakeiphoneapps.com/how-to-
asynchronously-add-web-content-to-uitableview-in-ios/1732/";
LinkShortener *linkShortener = [[LinkShortener alloc] init];
NSString *shortURL = [linkShortener getTheShortURLVersionOfThisLongURL:longURL];
NSLog(@"shortURL = %@", shortURL);
}
return 0;
}
See Listings 7-2 through 7-4 for the code.
#import <Foundation/Foundation.h>
@interface LinkShortener : NSObject<NSXMLParserDelegate>{
@private
NSMutableString *recorderString;
NSString *currentElement;
}
-(NSString *)getTheShortURLVersionOfThisLongURL:(NSString *)longURL;
@end
#import "LinkShortener.h"
@implementation LinkShortener
-(NSString *)getTheShortURLVersionOfThisLongURL:(NSString *)longURL{
#warning Get your API Login from https://bitly.com/ and put it here before running
NSString *APILogin = @"[YOUR API LOGIN]";
#warning Get your API key from https://bitly.com/ and put it here before running
NSString *APIKey = @"[YOUR API KEY]";
NSString *requestString = [[NSString alloc] initWithFormat:
@"http://api.bit.ly/shorten?version=2.0.1
&longUrl=%@&login=%@&apiKey=%@&format=xml",
longURL, APILogin, APIKey];
NSURL *requestURL = [NSURL URLWithString:requestString];
recorderString = nil;
NSData *responseData = [NSData dataWithContentsOfURL:requestURL];
NSXMLParser *xmlParser = [[NSXMLParser alloc] initWithData:responseData];
xmlParser.delegate = self;
[xmlParser parse];
if(recorderString)
return [recorderString copy];
else
return nil;;
}
- (void) parser:(NSXMLParser *)parser
didStartElement:(NSString *)elementName
namespaceURI:(NSString *)namespaceURI
qualifiedName:(NSString *)qName
attributes:(NSDictionary *)attributeDict{
currentElement = [elementName copy];
if ([elementName isEqualToString:@"shortUrl"]) {
recorderString = [[NSMutableString alloc] init];
}
}
- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string{
if ([currentElement isEqualToString:@"shortUrl"])
[recorderString appendString:string];
}
@end
#import <Foundation/Foundation.h>
#import "LinkShortener.h"
int main(int argc, const char * argv[]){
@autoreleasepool {
NSString *longURL = @"http://howtomakeiphoneapps.com/
how-to-asynchronously-add-web-content-to-uitableview-in-ios/1732/";
LinkShortener *linkShortener = [[LinkShortener alloc] init];
NSString *shortURL = [linkShortener getTheShortURLVersionOfThisLongURL:longURL];
NSLog(@"shortURL = %@", shortURL);
}
return 0;
}
To use the URL shortener function, import the header file into the class where you want to use the functionality. Then instantiate a LinkShortener
object from the LinkShortener
class. Finally, to use the function, send the message getTheShortURLVersionOfThisLongURL:
with the long URL as a parameter to the LinkShortener
object. The function will return the shortened URL if the web request was successful and nil
if the request was not successful. Here is what you should see in your own console log:
shortURL = http://bit.ly/yFmJFh
NOTE: The shortened URL that you receive back from bitly may not look exactly like the one I received when I tested this code.
You would like to add web services that use JSON data to your application.
NOTE: JSON is an alternative to XML that many Internet companies use when implementing web services. JSON stands for JavaScript Object Notation and is used for data storage and transportation. Web services that are implemented as REST (REpresentational State Transfer) web services provide both XML and JSON response data. Other types of web services may only provide one or the other.
As in Recipe 7.2, formulate a request string based on the documentation that the publisher of the web service provides. Create an NSURL
object based on the request string and NSData
to download the response from the web service. Use NSJSONSerialization
to parse the JSON data that you get back.
NOTE: NSJSONSerialization
is available starting with Mac OSX 10.7 and iOS 5.0.
This recipe uses the same bitly web service and requires the same process to request and download the results as was shown in Recipe 7.2. However, NSJSONSerialization
doesn’t use delegation, so you don’t need to add a new file or class to accommodate JSON parsing with NSJSONSerialization
.
Since you don’t need a separate class for this, you can locate the code needed to construct the request string wherever in your application you need to use the web service. If you continue to use a command-line Mac app, this code could go right into the main.m
file. Here is how to construct the request string:
NSString *longURL = @"http://howtomakeiphoneapps.com/
how-to-asynchronously-add-web-content-to-uitableview-in-ios/1732/";
#warning Get your API Login from https://bitly.com/ and put it here before running
NSString *APILogin = @"[YOUR API LOGIN]";
#warning Get your API key from https://bitly.com/ and put it here before running
NSString *APIKey = @"[YOUR API KEY]";
NSString *requestString = [[NSString alloc] initWithFormat:
@"http://api.bit.ly/shorten?version=2.0.1
&longUrl=%@&login=%@&apiKey=%@&format=json", longURL, APILogin, APIKey];
Then you can use NSURL
and NSData
to get the response based on this request string.
NSURL *requestURL = [NSURL URLWithString:requestString];
NSData *responseData = [NSData dataWithContentsOfURL:requestURL];
Next you’ll see how to parse JSON. Instead of the tagged data scheme that XML uses, JSON organizes content based on two structures: a collection of name-value pairs and an ordered list of values. A JSON collection of name-value pairs follows the same pattern as an Objective-C NSDictionary
while a JSON order list of values corresponds to an Objective-C NSArray
.
If you look at a JSON file, you will see these types of structures organized by curly braces instead of the tagged data in an XML file. For example, the JSON version of the Person
element described in Recipe 7.2 looks like this:
{"Person":"Matthew J. Campbell","Gender":"Male"}
Since JSON data is keyed in this way, and the dictionary and array structures map so well to programming languages, JSON is generally much easier to parse. As you’ll see in a moment, you have a Foundation function at your disposal that simply turns your JSON response into an NSDictionary
with the JSON content ready for you to use.
The first thing you need is an NSError
object, which you pass with the JSONObjectWithData:options:error:
message that must be sent to NSJSONSerialization
.
NSError *error = nil;
NSDictionary *bitlyJSON = [NSJSONSerialization JSONObjectWithData:responseData
options:0
error:&error];
The result of this function is assigned to an NSDictionary
that holds the contents of the JSON data. To get to the content that you need, simply access the various objects that are in the dictionary. This often requires you to reference nested dictionaries, arrays, and objects. You need to examine the response data to figure out precisely what you need. Here is what you do to get the bitly short URL from your response data:
if(!error){
NSDictionary *results = [bitlyJSON objectForKey:@"results"];
NSDictionary *resultsForLongURL = [results objectForKey:longURL];
NSString *shortURL = [resultsForLongURL objectForKey:@"shortUrl"];
NSLog(@"shortURL = %@", shortURL);
}
else{
NSLog(@"There was an error parsing the JSON");
}
See Listing 7-5 for the code.
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]){
@autoreleasepool {
NSString *longURL = @"http://howtomakeiphoneapps.com/
how-to-asynchronously-add-web-content-to-uitableview-in-ios/1732/";
#warning Get your API Login from https://bitly.com/ and put it here before
running
NSString *APILogin = @"[YOUR API LOGIN]";
#warning Get your API key from https://bitly.com/ and put it here before
running
NSString *APIKey = @"[YOUR API KEY]";
NSString *requestString = [[NSString alloc] initWithFormat:
@"http://api.bit.ly/shorten?version=2.0.1
&longUrl=%@&login=%@&apiKey=%@&format=json",
longURL, APILogin, APIKey];
NSURL *requestURL = [NSURL URLWithString:requestString];
NSData *responseData = [NSData dataWithContentsOfURL:requestURL];
NSError *error = nil;
NSDictionary *bitlyJSON = [NSJSONSerialization JSONObjectWithData:responseData
options:0
error:&error];
if(!error){
NSDictionary *results = [bitlyJSON objectForKey:@"results"];
NSDictionary *resultsForLongURL = [results objectForKey:longURL];
NSString *shortURL = [resultsForLongURL objectForKey:@"shortUrl"];
NSLog(@"shortURL = %@", shortURL);
}
else{
NSLog(@"There was an error parsing the JSON");
}
}
return 0;
}
You can use this code from any area in your application. If you are testing this with a Mac command-line application, you can simply include this code in your main.m
file, but you need to obtain an API login and API login from bitly. Examine the console log window to see the results of the web service request. You should see something like this:
shortURL = http://bit.ly/yFmJFh
NOTE: JSON requires far fewer steps than XML and will probably be your first choice when working with web services (when available). However, be aware that JSON may not always be available from the web service and that you must be using Mac OSX 10.7 or iOS 5 or greater to use JSON.
You want to be able to consume web content as a background process so that the network activity doesn’t affect your user interface.
Use NSURLConnection
and NSURLRequest
when you want to work with the network asynchronously or if you need more control over the process of using network connections and web requests.
The first thing you need is a request string to send to a web server. You could use a request string for a web service (as you did in Recipes 7.2 and 7.3) or you could even put in a web page. For this example, I’ll use the RSS feed for my blog since I know it will provide some XML data to download.
NOTE: RSS stands for Really Simple Syndication. RSS is used for publishing content like blog posts and podcasts. RSS files are usually large files that are based on XML but also follow additional specifications that help when publishing content. Adding an RSS feed to your application is an easy way to publish information to your users since most blogging software comes with built-in RSS features.
For this recipe, I’m going to use a Mac Cocoa application. You could also use an iOS application here, but you will have problems if you attempt to use a command-line Mac application. This is because the asynchronous methods used with NSURLConnection
may take longer to execute than the Mac command-line application’s lifecycle, so you may never see the results in the console log.
The code is located in the Mac Cocoa application’s AppDelegate.m
file right in the applicationDidFinishLaunching:
delegate method. The first thing you need is the request string (which, in this example, is my blog’s RSS feed).
NSString *requestString = @"http://www.howtomakeiphoneapps.com/feed/";
Then you can construct an NSURL
object based on the request string.
NSURL *requestURL = [NSURL URLWithString:requestString];
Use the requestURL
object to instantiate a new NSURLRequest
.
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:requestURL
cachePolicy:NSURLRequestReload
IgnoringLocalCacheData
timeoutInterval:10];
You also get a chance to specify how you want the request to handle caching and a timeout interval. Next, you need to set up an NSOperationQueue
, which will be used with NSURLConnection
to execute the web request.
NSOperationQueue *backgroundQueue =[[NSOperationQueue alloc] init];
Finally, use a class method to execute your web request asynchronously. You need three parameters: the NSURLRequest
object, the NSOperationQueue
object, and a code block. The code block gives you chance to let NSURLConnection
know what code to execute after the data is retrieved.
[NSURLConnection sendAsynchronousRequest:request
queue:backgroundQueue
completionHandler:^(NSURLResponse *response, NSData *data,
NSError *error) {
if(!error){
NSString *requestResults = [[NSString alloc] initWithData:data
encoding:NSStringEncodingConversionAllowLossy];
NSLog(@"requestResults=%@", requestResults);
}
else
NSLog(@"error=%@", error);
}];
NOTE: This feature is only available starting with Mac OSX 10.7 and iOS 5.0.
Take a close look at the completionHandler
block to see how to handle the NSData
object that is returned. Usually you test the NSError
object to see if everything went well before processing the data. Here all you are doing is writing out the entire RSS feed to the console log, but if you want to process the RSS feed, you can set up an XML parser like the one detailed in Recipe 7.2. See Listings 7-6 through 7-8 for the code.
#import <Cocoa/Cocoa.h>
int main(int argc, char *argv[]){
return NSApplicationMain(argc, (const char **)argv);
}
#import <Cocoa/Cocoa.h>
@interface AppDelegate : NSObject <NSApplicationDelegate>
@property (assign) IBOutlet NSWindow *window;
@end
#import "AppDelegate.h"
@implementation AppDelegate
@synthesize window = _window;
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification{
NSString *requestString = @"http://www.howtomakeiphoneapps.com/feed/";
NSURL *requestURL = [NSURL URLWithString:requestString];
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:
requestURL cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:10];
NSOperationQueue *backgroundQueue =[[NSOperationQueue alloc] init];
[NSURLConnection sendAsynchronousRequest:request
queue:backgroundQueue
completionHandler:^(NSURLResponse *response, NSData *data,
NSError *error) {
if(!error){
NSString *requestResults = [[NSString alloc] initWithData:data
encoding:NSStringEncodingConversionAllowLossy];
NSLog(@"requestResults=%@", requestResults);
}
else{
NSLog(@"error=%@", error);
}
}];
}
@end
To try this recipe out, include the code in the app delegate of a Mac or iOS application. Inspect the log to see the data that was downloaded from the Web. To test the error handling, disconnect your Mac from the network and inspect the log to see what the error object reports. If your web request was successful, you should see something like this appear in your console log:
requestResults=<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
xmlns:content="http://purl.org/rss/1.0/modules/content/"
xmlns:wfw="http://wellformedweb.org/CommentAPI/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
>
<channel>
<title>How to Make iPhone Apps</title>
<atom:link href="http://howtomakeiphoneapps.com/feed/" rel="self"
type="application/rss+xml" />
. . .
This code can be included anywhere in your application where you want to consume web content, but don’t interfere with or block other processes like your user interface.
3.149.27.72