Connecting to the Network - Learning iPhone Programming (2010)

Learning iPhone Programming (2010)

Chapter 7. Connecting to the Network

The iPhone and iPod touch platforms are designed for always-on connectivity in mind. Developers have taken advantage of this to create some innovative third-party applications. Most iPhone applications will make a network connection at some point, and many are so fundamentally tied to web services that they need a network connection to function.

Detecting Network Status

Before your application attempts to make a network connection, you need to know whether you have a network available, and depending on what you want to do you might also want to know whether the device is connected to a WiFi or cellular network.

Warning

One of the more common reasons for Apple to reject an application submitted for review is that the application doesn’t correctly notify the user when the application fails to access the network. Apple requires that you detect the state of the network connection and report it to the user when the connection is unavailable, or otherwise handle it in a graceful manner.

Apple’s Reachability Class

Helpfully, Apple has provided some sample code to deal with detecting current network status. The Reachability code is available at http://developer.apple.com/iphone/library/samplecode/Reachability/.

Warning

Two different versions of the Apple Reachability code are in general circulation. The earlier version, which appears in many web tutorials and has been widely distributed, dates from the pre-2.0 SDK. The newer version, released in August 2009, is much improved and supports asynchronous connection monitoring. However, the interface offered by the two versions is very different, so to avoid confusion you need to be aware which version of the Reachability code you’re using.

Download the Reachability.zip file from Apple, and unzip it. Open the Reachability/Classes directory and grab the Reachability.h and Reachability.m files from the Xcode project and copy them onto your Desktop (or any convenient location). This is the Reachabilityclass that we want to reuse in our projects.

To use the Reachability class in a project, you must do the following after you create the project in Xcode:

1. Drag and drop both the header and implementation files into the Classes group in your project, and be sure to tick the “Copy items into destination group’s folder (if needed)” checkbox in the pop-up dialog that appears when you drop the files into Xcode.

2. Right-click or Ctrl-click on the Frameworks group, select Add→Existing Frameworks, and then select SystemConfiguration.framework in the Frameworks selector pop up, as shown in Figure 7-1. The Reachability code needs this framework and it has to be added to your projects where you use it.

Selecting SystemConfiguration.framework from the list offered by Xcode when adding a new framework to a project

Figure 7-1. Selecting SystemConfiguration.framework from the list offered by Xcode when adding a new framework to a project

There are two ways to make use of Apple’s Reachability code: synchronously or asynchronously.

Synchronous reachability

The synchronous case is the simpler of the two approaches; here we import the Reachability.h header file into our code and then carry out a “spot-check” as to whether the network is reachable, and whether we have a wireless or WWAN connection:

#import "Reachability.h"

... some code omitted ...

Reachability *reach = [[Reachability reachabilityForInternetConnection] retain];

NetworkStatus status = [reach currentReachabilityStatus];

or alternatively, whether a specific host is reachable:

Reachability *reach =

[[Reachability reachabilityWithHostName: @"www.apple.com"] retain];

NetworkStatus status = [reach currentReachabilityStatus];

We can then use a simple switch statement to decode the network status. The following code turns the status flag into an NSString, perhaps to update a UILabel in the application interface, but of course you can trigger any action you need to (disabling parts of your user interface, perhaps?) depending on the current network status:

- (NSString *)stringFromStatus:(NetworkStatus ) status {

NSString *string;

switch(status) {

case NotReachable:

string = @"Not Reachable";

break;

case ReachableViaWiFi:

string = @"Reachable via WiFi";

break;

case ReachableViaWWAN:

string = @"Reachable via WWAN";

break;

default:

string = @"Unknown";

break;

}

return string;

}

We can easily put together a quick application to illustrate use of the Reachability code. Open Xcode and start a new project. Choose a view-based iPhone OS application, and when prompted, name it “NetworkMonitor”. Import the Reachability code, add theSystemConfiguration.framework into your new project (as discussed in the preceding section), open the NetworkMonitorAppDelegate.h interface file in the Xcode editor, and declare the stringFromStatus: method as shown in the following code:

#import <UIKit/UIKit.h>

#import "Reachability.h"

@class NetworkMonitorViewController;

@interface NetworkMonitorAppDelegate : NSObject <UIApplicationDelegate> {

UIWindow *window;

NetworkMonitorViewController *viewController;

}

@property (nonatomic, retain) IBOutlet UIWindow *window;

@property (nonatomic, retain) IBOutlet

NetworkMonitorViewController *viewController;

- (NSString *)stringFromStatus:(NetworkStatus )status;

@end

Save your changes, and open the NetworkMonitorAppDelegate.m implementation file in the Xcode editor and modify the applicationDidFinishLaunching: method:

- (void)applicationDidFinishLaunching:(UIApplication *)application {

// Override point for customization after app launch

[window addSubview:viewController.view];

[window makeKeyAndVisible];

Reachability *reach =

[[Reachability reachabilityForInternetConnection] retain];

NetworkStatus status = [reach currentReachabilityStatus];

UIAlertView *alert = [[UIAlertView alloc]

initWithTitle:@"Reachability"

message:[self stringFromStatus: status]

delegate:self

cancelButtonTitle:@"OK"

otherButtonTitles:nil];

[alert show];

[alert release];

}

The final step is to add the stringWithStatus: method I showed earlier to NetworkMonitorAppDelegate.m. Save your changes and click the Build and Run button on the Xcode toolbar to compile your code and deploy it into iPhone Simulator. You should see something similar toFigure 7-2.

Asynchronous reachability

The asynchronous approach is (only slightly) more complicated, but using the Reachability class in this way means your application can be notified of changes in the current network status. You must first import the Reachability.h header file into your code. After that, you need to register the class that must monitor the network as an observer for the kReachabilityChangedNotification event:

[[NSNotificationCenter defaultCenter] addObserver: self

selector: @selector(reachabilityChanged:)

name: kReachabilityChangedNotification

object: nil];

The NetworkMonitor application in iPhone Simulator

Figure 7-2. The NetworkMonitor application in iPhone Simulator

Then you need to create a Reachability instance and start event notification:

Reachability *reach =

[[Reachability reachabilityWithHostName: @"www.apple.com"] retain];

[reach startNotifer];

When the network reachability status changes, the Reachability instance will notify your code by calling the reachabilityChanged: method. What you do in that method of course very much depends on why you’re monitoring the network status in the first place; however, the stub of such a method would look like this:

- (void) reachabilityChanged: (NSNotification *)notification {

Reachability *reach = [notification object];

if( [reach isKindOfClass: [Reachability class]]) {1

NetworkStatus status = [reach currentReachabilityStatus];

// Insert your code here

}

}

1

The isKindOfClass: method returns a Boolean that indicates whether the receiver is an instance of a given class. Here we check whether the Reachability object passed as part of the notification is indeed of type Reachability.

Using Reachability directly

The Apple Reachability class is just a friendly wrapper around the SCNetworkReachability programming interface, which is part of SystemConfiguration.framework. While I recommend using Apple’s sample code if possible, you can use the interfaces directly if you need to do something out of the ordinary.

Note

If you are interested in alternative approaches to checking for network reachability, I recommend looking at the UIDevice-Reachability extensions provided by Erica Sadun in The iPhone Developer’s Cookbook (Addison-Wesley). The Reachability code is available for download from the GitHub source repository that accompanies the book. It is located in the 013-Net⁠working/02-General Reachability/ folder of the repository.

Embedding a Web Browser in Your App

The UIWebView class allows you to embed web content inside your application. This class is the simplest but least flexible way of getting network content into your application. A UIWebView is best used to display content. If you want to manipulate the content programmatically, you should skip ahead a couple of sections and look at the discussion of the NSURLConnection class. However, there are a few tricks you can play to retrieve the displayed content from the UIWebView once it has been downloaded, and I’ll talk about them later in the section.

A Simple Web View Controller

There are a number of cases where you might want to load a URL and display a web page, but keep users inside your application rather than closing it and opening Safari. If this is what you need to do, you should be using a UIWebView.

So, let’s build some code that you’ll be able to reuse in your own applications later. The specification for this code is a view controller that we can display modally, which will display a UIWebView with a specified web page, and can then be dismissed, returning us to our application.

I’m going to prototype the code here, hanging it off a simple view with a button that will pull up the modal view. However, the view controller class is reusable without modification; just drag and drop the code out of this project and into another. This is also a good exercise in writing reusable code.

Open Xcode and start a new project, choose a view-based iPhone OS application, and when prompted, name it “Prototype”. The first thing we want to do is set up our main view; this is going to consist of a single button that we’ll click to bring up the web view. Click on thePrototypeViewController.h interface file to open it in the editor, and add a UIButton flagged as an IBOutlet and an associated method (flagged as an IBAction) to the interface file. The added code is shown in bold:

#import <UIKit/UIKit.h>

@interface PrototypeViewController : UIViewController {

IBOutlet UIButton *goButton;

}

-(IBAction) pushedGo:(id) sender;

@end

Now, open the PrototypeViewController.m implementation file and add a stub for the pushedGo: method. As always, you have to remember to release the goButton in the dealloc: method:

-(IBAction) pushedGo:(id) sender {

// Code goes here later

}

- (void)dealloc {

[goButton release];

[super dealloc];

}

Next, we need to add a new view controller class to the project. This is the class we’re going to use to manage our UIWebView. Right-click on the Classes group in the Groups & Files pane in Xcode and select Add→New File, select the UIViewController subclass template from the Cocoa Touch Class category, and check the “With XIB for user interface” box. When prompted, name the new class “WebViewController”.

Three files will be created: the interface file WebViewController.h, the implementation file WebViewController.m, and the view NIB file WebViewController.xib.

Note

At this point, I normally rename the NIB file, removing the “Controller” part of the filename and leaving it as WebView.xib, as I feel this is a neater naming scheme. I also usually move it from the Classes group to the Resources group to keep it with the other NIBs.

After creating this new view controller, we need to leave Xcode for a moment. Double-click on the PrototypeViewController.xib file to open the NIB file in Interface Builder. Drag and drop a round rect button (UIButton) into the view and change its text to something appropriate; I picked “Go!”. (You can find the button in the Inputs & Values category of the Library.)

Next, click on File’s Owner in the WebView.xib window. In the Connections Inspector (⌘-2), connect both the goButton outlet and the pushedGo: received action to the button that you just dropped into the view, choosing Touch Up Inside as the action; see Figure 7-3. Make sure you save your changes to the PrototypeViewController.xib file and close it. We’re done with the PrototypeViewController class for now.

Connecting the UIButton to File’s Owner, the PrototypeViewController

Figure 7-3. Connecting the UIButton to File’s Owner, the PrototypeViewController

Now we need to build our web view. Double-click on WebView.xib to open the NIB file in Interface Builder. Drag and drop a navigation bar (UINavigationBar) from Library→Windows, Views & Bars, and position it at the top of the view. Then drag a web view (UIWebView) from Library→Data Views into the view and resize it to fill the remaining portion of the View window. Check the box marked Scales Page to Fit in the Attributes Inspector (⌘-1). Finally, drag a bar button item (UIBarButton) onto the navigation bar, and in the Attributes tab of the Inspector window change its identifier to Done. Once you’re done, your view will look similar to Figure 7-4.

After saving the changes to the WebView.xib file, close it and return to Xcode. We now need to implement the WebViewController class before we can connect the new UI to our code.

Open the WebViewController.h interface file. We want to make this class self-contained so that we can reuse it without any modifications. Therefore, we’re going to override the init: function to pass in the URL when instantiating the object. Make the following changes to the file (notice that I’ve added <UIWebViewDelegate> to the interface declaration):

#import <UIKit/UIKit.h>

@interface WebViewController : UIViewController <UIWebViewDelegate> {

NSURL *theURL;

NSString *theTitle;

IBOutlet UIWebView *webView;

IBOutlet UINavigationItem *webTitle;

}

- (id)initWithURL:(NSURL *)url;

- (id)initWithURL:(NSURL *)url andTitle:(NSString *)string;

- (IBAction) done:(id)sender;

@end

Creating our web view in Interface Builder

Figure 7-4. Creating our web view in Interface Builder

In fact, to give a bit more flexibility to the class, I provided two different init: functions: initWithURL: and initWithURL:andTitle:. There’s also a done: method flagged as an IBAction that we can connect to our Done UIBarButtonItem when we go back into Interface Builder.

We’ve declared an NSURL and an NSString to store the URL and view title passed to our init methods, along with a UIWebView and a UINavigationItem flagged as IBOutlet to connect to the UI elements we created previously in Interface Builder.

Navigation Bars and Interface Builder

If you add the UINavigationBar to your modal view inside Interface Builder, as we have done here, it is not managed by a UINavigationController. This means you cannot set the title of the navigation bar inside your view controller using the self.title or theself.NavigationItem.title property.

There are several ways around this problem, but one of the easier ways is to declare a UINavigationItem IBOutlet in the view controller’s interface file, and then in Interface Builder connect this outlet to the UINavigationItem that contains the title (you’ll need to switch the WebView.xib window into list mode with Option-⌘-2 and expand the navigation bar).

Once this is done, you can set the title in the navigation bar from the viewDidLoad: or viewDidAppear: method using the title property of the instance variable pointing to your UINavigationItem IBOutlet variable that you declared.

Now, open the WebViewController.m implementation file. We’ll start by implementing the two initWith methods. Add the following code to the file:

- (id)initWithURL:(NSURL *)url andTitle:(NSString *)string {

if( self = [super init] ) {

theURL = url;

theTitle = string;

}

return self;

}

-(id)initWithURL:(NSURL *)url {

return [self initWithURL:url andTitle:nil];1

}

1

We implemented the initWithURL: method by calling the initWithURL:andTitle: method with an empty (nil) title. Doing it this way means that if we need to change the implementation of the initialization method later, we have to do so in only one place.

Next, we have to load the URL into the view, and we’ll do that in the viewDidLoad: method. Uncomment the viewDidLoad: method and add the lines shown in bold:

- (void)viewDidLoad {

[super viewDidLoad];

webTitle.title = theTitle;1

NSURLRequest *requestObject = [NSURLRequest requestWithURL:theURL];2

[webView loadRequest:requestObject];3

}

1

We set the title property of the NSNavigationBarItem to the title string we passed earlier in the initWithURL:andTitle: method.

2

We marshal (gather together along with the other necessary data) the URL we passed in the initWithURL:andTitle: method to form an NSURLRequest object.

3

Here, we load the requested URL into the UIWebView.

Now we have to deal with what happens when the user dismisses the view by tapping the Done button. Add the following to the file:

- (IBAction) done:(id)sender {

[self dismissModalViewControllerAnimated:YES];1

}

- (void)viewWillDisappear:(BOOL)animated {

[super viewWillDisappear:animated];

webView.delegate = nil;2

[webView stopLoading];3

}

1

In the done: method, we dismiss the modal view, which will trigger the viewWillDi⁠sappear:animated: method.

2

In the viewWillDisappear:animated: method, we have to set the delegate class for our UIWebView to nil. We’re about to deallocate our object, and if we don’t set the delegate property to nil before this happens, messages sent to the nonexistent object by theUIWebViewDelegate protocol will cause our application to crash.

3

We also have to stop loading from our URL because the events generated as the web page continues to load will cause the application to crash.

Finally, we have to make sure we release our declared variables in the dealloc: method. Add the lines shown in bold to this method:

- (void)dealloc {

[webView release];

[webTitle release];

[super dealloc];

}

We’re not quite done yet. Back in the PrototypeViewController.m file we still need to implement the pushedGo: method. Replace // Code goes here later with the code shown in bold:

-(IBAction) pushedGo:(id)sender {

NSURL *url = [NSURL URLWithString:@"http://www.apple.com/"];

WebViewController *webViewController =

[[WebViewController alloc] initWithURL:url andTitle:@"Apple"];1

[self presentModalViewController:webViewController animated:YES];2

[webViewController release];

}

1

We create an instance of our WebViewController using initWithURL:andTitle:.

2

We present the new controller modally.

Remember that since we’ve used the class in the pushedGo: method, we also now need to import the WebViewController.h header file into the PrototypeViewController. So, go to the top of PrototypeViewController.m and add this line:

#import "WebViewController.h"

We’re done in Xcode. Now we have to go back into Interface Builder and connect the web view to our controller code. Open the WebView.xib file in Interface Builder. Make sure you are in List view mode (Option-⌘-2) and expand the view completely, then click on File’s Owner. In the Connection Inspector:

1. Connect the webTitle outlet to the UINavigationItem “Navigation Item (Title)”.

2. Connect the webView outlet to the UIWebView “Web View”.

3. Connect the done: received action to the UIBarButtonItem “Bar Button Item (Done)”.

Finally, click on the web view and connect the delegate outlet back to File’s Owner.

At this point, if you click on File’s Owner in the main NIB window and check the Connections tab, you should see something similar to Figure 7-5.

Save the NIB and return to Xcode. Click on the Build and Run button in the Xcode toolbar to compile and start the application in iPhone Simulator, as shown in Figure 7-6. Tap the Go! button and the Apple website should load in your view. Remember that you’re making a network connection here, so you might have to be a bit patient depending on the speed of your network connection.

Of course, users don’t like to be patient, and we currently don’t have a way to indicate to them that our application is doing something they need to be patient about. This is where the UIWebViewDelegate protocol comes in; we declared WebViewController as a web view delegate, but so far we haven’t taken advantage of that.

The delegate protocol offers two methods: webViewDidStartLoad: and webViewDidFi⁠nishLoad:. We can use these to start and stop the network activity indicator in the iPhone’s toolbar to indicate that we’re transferring data and the user should be patient. Add these two methods toWebViewController.m:

- (void)webViewDidStartLoad:(UIWebView *)wv {

[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;

}

- (void)webViewDidFinishLoad:(UIWebView *)wv {

[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;

}

But what happens if our URL fails to load? Even if we checked reachability before creating the view controller, what if we lose the network connection while the page itself is loading? The delegate protocol also provides the webView:didFailLoadWithEr⁠ror: method to inform us that something has gone wrong. Add the following to WebViewController.m:

- (void)webView:(UIWebView *)wv didFailLoadWithError:(NSError *)error {

[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;

NSString *errorString = [error localizedDescription];

NSString *errorTitle =

[NSString stringWithFormat:@"Error (%d)", error.code];

UIAlertView *errorView = [[UIAlertView alloc]

initWithTitle:errorTitle

message:errorString

delegate:self

cancelButtonTitle:nil

otherButtonTitles:@"OK", nil];1

[errorView show];

[errorView autorelease];

}

1

Here we grab the error description and open an alert view to display the error. We declare our view controller class to be the alert’s delegate.

The web view NIB file connected to the WebViewController

Figure 7-5. The web view NIB file connected to the WebViewController

The initial main view (left) and the web view (right)

Figure 7-6. The initial main view (left) and the web view (right)

Since we said our view controller class is the UIAlertView delegate, we also have to declare the class as a UIAlertViewDelegate in the WebViewController.h interface file:

@interface WebViewController :

UIViewController <UIWebViewDelegate, UIAlertViewDelegate> {

... no changes to the code inside the declaration ...

}

With this change made, we can make use of the UIAlertViewDelegate protocol back in our implementation to dismiss the web view pane when an error is received loading our URL. Add the following to WebViewController.m:

- (void)didPresentAlertView:(UIAlertView *)alertView {

[self dismissModalViewControllerAnimated:YES];

}

We’re done. With these changes, the application can tell the user that it is doing something, and can handle any errors that occur when loading the URL. Click on the Build and Run button in the Xcode toolbar to compile and start the application in iPhone Simulator. Tap the Go! button and you should see the activity indicator spinning in the toolbar next to the WiFi signal strength icon as the application loads Apple’s web page. When it finishes, the spinner should stop.

Click Done, and then either turn Airport off or unplug your Ethernet cable. Now try again, and you should get something that looks like Figure 7-7, informing you that you no longer have an Internet connection.

The webView:didFailLoadWithError: method creates a UIAlertView and dismisses the web view when we fail to load the URL we passed to it

Figure 7-7. The webView:didFailLoadWithError: method creates a UIAlertView and dismisses the web view when we fail to load the URL we passed to it

At this point, you have a reusable WebViewController class and associated NIB file that you can copy and drop directly into your own projects. You might also want to think about improvements if you do that, of course. For instance, the only error checking we do occurs after we attempt to load the view. Perhaps you could make use of the Reachability class we looked at earlier in the chapter inside the viewWillAppear: method, before the web view is even displayed, to check the network connection. Then you can pop up an alert view if you are unable to reachtheURL (which we passed to the view controller as part of the initWithURL: or initWithURL:andTitle: method) before the view is displayed to the user rather than afterward.

Displaying Static HTML Files

We can use the UIWebView class to display HTML files bundled into our project. In fact, we can add HTML documents to our project in the same way we dragged and dropped the images into the City Guide application; see Adding Images to Your Projects in Chapter 5.

Suppose we’re going to use a web view to display a help document for our application. We could do so as follows:

NSString *filePath =

[[NSBundle mainBundle]

pathForResource:@"HelpDocument" ofType:@"html"];

NSData *htmlData = [NSData dataWithContentsOfFile:filePath];

if (htmlData) {

[webView loadData:htmlData

MIMEType:@"text/html"

textEncodingName:@"UTF-8"

baseURL:[NSURL URLWithString:@"http://www.babilim.co.uk"]];

}

We grab the file path to our bundled resource, create an NSData object, and pass this to our web view.

Embedding Images in the Application Bundle

Since we can specify the base URL of our web view, we can use a trick to embed small images directly into our application bundle by setting this base URL for our HTML document correctly. For instance, if we have an HTML document in the NSString variable htmlDocument, we could add this snippet:

NSString *filePath = [[NSBundle mainBundle] bundlePath];

NSURL *baseURL = [NSURL fileURLWithPath:filePath];

[webView loadHTMLString:htmlDocument baseURL:baseURL];

This will load the HTML document into our UIWebView. However, it will also set the base URL to the root of the application bundle and allow us to add images (or other content) into our application and refer to them directly in our document (or an associated CSS file):

<img src="image.png">

You should note that even if you store your images inside a folder in your Xcode project, they will be at the root of the application bundle file when you build and deploy your application.

Getting Data Out of a UIWebView

A UIWebView is primarily intended to display a URL, but if need be you can retrieve the content that has been loaded into the view using the stringByEvaluatingJavaScriptFromString: method:

NSString *content =

[webView stringByEvaluatingJavaScriptFromString:@"document.body.outerHTML"];1

1

Here we retrieve the contents of the HTML <body> ... </body> tag.

Sending Email

The MFMailComposeViewController class provides access to the same interface used by the Mail client to edit and send an email. The most common way to present this interface is to do so modally using the presentModalViewController:animated: method, just as we did in the preceding section to create a reusable web view class.

We can therefore reuse our Prototype application code from the preceding section to demonstrate how the mail composer works; we’ll just drop in a class that displays the mail interface instead of the web interface. Open the Finder and navigate to the location where you saved thePrototype project. Right-click on the folder containing the project files and select Duplicate; a folder called Prototype copy will be created containing a duplicate of our project. Rename the folder Prototype2, and then open the new (duplicate) project inside Xcode and use the Project→Rename tool to rename the project itself.

Next, prune back the code:

1. Open the copy of the project in Xcode and delete the WebViewController.h, WebViewController.m, and WebView.xib files by right-clicking on each file in the Groups & Files pane and selecting Delete from the pop-up menu. When prompted, click Also Move to Trash. If you moved WebView.xib into your Resources folder with the rest of the NIBs, look for it there.

2. Now click on the PrototypeViewController.m file to open it in the editor. Delete the line where you import the WebViewController.h file and delete all the code in the pushedGo: method, but not the method itself.

At this point, we have just the stub of the application, with that Go! button and associated pushedGo: method we can use to trigger the display of our mail composer view. So, let’s write the code to do that now.

The first thing we need to do is add the MessageUI.framework framework to the project. As you did earlier for the SystemConfiguration.framework, right-click on the Frameworks group and select Add→Existing Frameworks. Then select the MessageUI.framework from the list presented in the framework selection pop-up window.

Warning

If you have upgraded your Xcode (and iPhone SDK) distribution in the middle of developing a project, MessageUI.framework may not show up in the list of frameworks presented to you by Xcode in the framework selection pop up. If this turns out to be the case, you may be able to resolve the problem by opening the Targets group in the Groups & Files pane in Xcode, right-clicking on the application’s target, and selecting Get Info. Navigate to the Build pane of the Target Info window and set the Base SDK of your project to the SDK you currently have installed (rather than the SDK with which you initially developed the project).

We’re going to present our mail composer view when the Go! button is clicked using our pushedGo: method. However, before we do, we need to see if the device is even configured to send email, using the canSendMail: class method. If it isn’t, we need to inform the user that the device isn’t able to send mail. When writing a real application that relies on email being available, you might want to do this check when the application starts inside your application delegate, and then either inform the user that there is a problem or disable the parts of your application that depend on it being able to send mail. Add the following code to the pushedGo: method in PrototypeViewController.m:

-(IBAction) pushedGo:(id)sender {

if (![MFMailComposeViewController canSendMail]) {1

NSString *errorTitle = @"Error";

NSString *errorString =

@"This device is not configured to send email.";

UIAlertView *errorView = [[UIAlertView alloc] initWithTitle:errorTitle

message:errorString

delegate:self

cancelButtonTitle:nil

otherButtonTitles:@"OK", nil];

[errorView show];

[errorView release];

} else {

MFMailComposeViewController *mailView =

[[[MFMailComposeViewController alloc] init] autorelease];2

mailView.mailComposeDelegate = self;3

[mailView setSubject:@"Test"];

[mailView setMessageBody:@"This is a test message" isHTML:NO];

[self presentModalViewController:mailView animated:YES];4

}

}

1

Here we check to see if the device is capable of sending mail. If it isn’t, we present a UIAlertView to inform the user.

2

We allocate and initialize an instance of the mail composer view controller.

3

We set the delegate for the controller to be this class, which implies that we have to implement the delegate protocol for the mail composer view controller.

4

After setting the subject and the message body, we present the view controller modally.

Unlike the web view we implemented earlier in the chapter, the mail composer view won’t dismiss itself when the user clicks the Send or Cancel button. We need to know when it is dismissed by the user; for that to happen we must implement theMFMailComposeViewControllerDelegate protocol. We therefore need to import the framework headers into the PrototypeViewController.h interface file, which we do by importing the MessageUI.h header file:

#import <MessageUI/MessageUI.h>

We also have to declare our PrototypeViewController as a delegate class for the mail view by changing the declaration in PrototypeViewController.h, as shown here:

@interface PrototypeViewController : UIViewController

<MFMailComposeViewControllerDelegate> {

... no changes to the code in here ...

}

The delegate protocol implements only one method, which dismisses the view controller and handles any errors: the mailComposeController:didFinishWithRe⁠sult:error: method. Let’s implement that now as part of our PrototypeViewController class. Add the following method to PrototypeViewController.m:

-(void)mailComposeController:(MFMailComposeViewController *)controller

didFinishWithResult:(MFMailComposeResult)result error:(NSError *)error {

if (error) {1

NSString *errorTitle = @"Mail Error";

NSString *errorDescription = [error localizedDescription];

UIAlertView *errorView = [[UIAlertView alloc]

initWithTitle:errorTitle

message:errorDescription

delegate:self

cancelButtonTitle:nil

otherButtonTitles:@"OK", nil];

[errorView show];

[errorView release];

} else {2

// Add code here to handle the MFMailComposeResult

}

[controller dismissModalViewControllerAnimated:YES];3

}

1

If the controller returns an error, we use a UIAlertView to inform the user.

2

If no error is returned, we should handle the MFMailComposeResult instead.

3

In either case, we need to dismiss the controller’s view and release the controller.

Before we discuss how to handle the MFMailComposeResult, let’s test our code. Click the Build and Go button on the Xcode toolbar to compile and start the application in iPhone Simulator. Once the application opens, click the Go! button. If all goes well, you should see something very similar to Figure 7-8.

Now that the application is working, let’s handle that MFMailComposeResult. The simplest way to illustrate how to handle the result is to add a label to the PrototypeViewController NIB file, and display the result returned by the mail composer view there.

The first thing you need to do is to add a UILabel to the PrototypeViewController.h interface file and declare it as an IBOutlet. Add the line shown in bold:

#import <UIKit/UIKit.h>

#import <MessageUI/MessageUI.h>

@interface PrototypeViewController : UIViewController

<MFMailComposeViewControllerDelegate> {

IBOutlet UIButton *goButton;

IBOutlet UILabel *resultLabel;

}

-(IBAction) pushedGo:(id)sender;

@end

The MFMailMailComposeViewController

Figure 7-8. The MFMailMailComposeViewController

Remember that now we’ve declared the label variable, so we also need to release it inside the dealloc: method. Add the following to the dealloc: method in PrototypeViewController.m:

[resultLabel release];

We also need to open the PrototypeViewController.xib file in Interface Builder and add the label. Open the NIB file and then drag and drop a label (UILabel) from the Library window onto the view. Now right-click on File’s Owner and connect the resultLabel outlet to the newUILabel. Make sure you save your changes to the NIB file, and then return to Xcode.

Now we can use the label to display the results. Inside the mail composer delegate method, replace the line that reads // Add code here to handle the MFMailComposeRe⁠⁠sult with the following code:

NSString *string;

switch (result) {

case MFMailComposeResultSent:

string = @"Mail sent.";

break;

case MFMailComposeResultSaved:

string = @"Mail saved.";

break;

case MFMailComposeResultCancelled:

string = @"Mail cancelled.";

break;

case MFMailComposeResultFailed:

string = @"Mail failed.";

break;

default:

string = @"Unknown";

break;

}

resultLabel.text = string;

The switch statement we just added enumerates the possible results, and then sets the label string to a human-readable result. We’re done. If you build the application again and send an email from the composer view, you should see something very much like Figure 7-9.

We successfully sent the mail message

Figure 7-9. We successfully sent the mail message

Attaching an Image to a Mail Message

You can attach an image to your mail message by using the addAttachmentData:mimeType:Filename: method. This should be called before displaying the mail composer interface, directly after the call to the setMessageBody:isHTML: method. You should not call this method after displaying the composer interface to the user.

If necessary, you can change the image type using the UIImageJPEGRepresentation() or UIImagePNGRepresentation() UIKit function, as shown here:

UIImage *image = [UIImage imageNamed:@"Attachment.png"];

NSData *data = UIImageJPEGRepresentation(image, 1.0);

[mailView addAttachmentData:data mimeType:@"image/jpeg"

fileName:@"Picture.jpeg"];

This example will look for Attachment.png at the root of the application bundle (to put a file there, drag it into the top level of the Groups & Files pane), convert it to a JPEG, and attach it under the filename Picture.jpeg.

Getting Data from the Internet

If you want to retrieve data from the Internet and process it programmatically, rather than just display it in a view, you should use the NSURLConnection class. While it’s more complicated than the UIWebView we looked at earlier in the chapter, it’s inherently more flexible.

The NSURLConnection class can make both synchronous and asynchronous requests to download the contents of a URL, and the associated delegate methods provide feedback and control for asynchronous requests.

Synchronous Requests

The easiest, but not the best, way to use the NSURLConnection class is to make a synchronous request for data:

NSString *url = @"http://www.apple.com";

NSURLRequest *request =

[NSURLRequest requestWithURL:[NSURL URLWithString:url]];

NSURLResponse *response = nil;

NSError *error = nil;

NSData *content = [NSURLConnection sendSynchronousRequest:request

returningResponse:&response error:&error];

NSString *string = [[NSString alloc] initWithData:content

encoding:NSUTF8StringEncoding];

NSLog(@"response: %@", string);

sendSynchronousRequest: is a convenience method built on top of the asynchronous request code. It’s important to note that if you use this method the calling thread will block until the data is loaded or the request times out. If the calling thread is the main thread of your application, your application will freeze while the request is being made. This is generally considered not a good thing from a UI perspective; I strongly encourage you to use the asynchronous connection and associated delegate methods.

Asynchronous Requests

Most of the time when you use the NSURLConnection class, you’ll make asynchronous requests this way:

NSString *string = [NSString stringWithFormat:@"http://www.apple.com/];

NSURL *url = [[NSURL URLWithString:string] retain];

NSURLRequest *request = [NSURLRequest requestWithURL:url];

[[NSURLConnection alloc]

initWithRequest:request delegate:self];1

1

Here we make the asynchronous call and set the delegate class to be self.

For this to work, you need to implement the following methods at a minimum. We’ll take a closer look at NSURLConnection in Using Web Services:

- (NSURLRequest *)connection:(NSURLConnection *)connection

willSendRequest:(NSURLRequest *)request

redirectResponse:(NSURLResponse *)redirectResponse

{

return request;

}

- (void)connection:(NSURLConnection *)connection

didReceiveResponse:(NSURLResponse *)response

{

[responseData setLength:0];1

}

- (void)connection:(NSURLConnection *)connection

didReceiveData:(NSData *)data

{

[responseData appendData:data];2

}

- (void)connection:(NSURLConnection *)

connection didFailWithError:(NSError *)error

{

... implementation code would go here ...

}3

- (void)connectionDidFinishLoading:(NSURLConnection *)connection {

... implementation code would go here ...

}4

1

You need to declare an NSMutableData variable (responseData in this example) in the interface file of your delegate class to hold the response data. As a stylistic choice, you may prefer to alloc and init your NSMutableData object, rather than calling the setLength: method as we have done here.

2

This appends the data as it is received to the response data variable. There may be multiple calls to this delegate method as data comes in from the response.

3

This method is called if an error occurs during the connection.

4

This method is called if the connection completes successfully.

Using Web Services

With the (re)emergence of REpresentational State Transfer (REST) as the dominant paradigm for modern web service architectures, chiefly championed by emerging Web 2.0 companies and platforms, the number of available services has grown significantly over the past few years.

Note

If you are interested in learning more about RESTful web services, I recommend the book RESTful Web Services by Leonard Richardson and Sam Ruby (O’Reilly).

The Google Weather Service

To illustrate the NSURLConnection class, we’re going to look at one of these RESTful services, the (mostly undocumented, as far as I can tell) Google Weather Service. A request to the Google Weather API of the form http://www.google.com/ig/api?weather=QUERY_STRING will return forecasts with temperatures in Fahrenheit; the same request to www.google.co.uk will return a forecast with temperatures in Centigrade.

Warning

While the Google Weather Service is a simple little service that has been around for some time in its current form, there is very little documentation surrounding it. As such, Google may not regard it as an “officially supported” API and the service may be subject to change without much notice.

So, for instance, if we made a request of the Google Weather API for the current conditions and forecast in London, the request would look like http://www.google.com/ig/api?weather=London,UK. If we do that, the service will return an XML document containing the current and forecasted conditions:

<?xml version="1.0"?>

<xml_api_reply version="1">

<weather module_id="0" tab_id="0"

mobile_row="0"

mobile_zipped="1"

row="0"

section="0" >

<forecast_information>

<city data="London, England"/>

<postal_code data="London,UK"/>

<latitude_e6 data=""/>1

<longitude_e6 data=""/>

<forecast_date data="2009-08-29"/>

<current_date_time data="2009-08-29 17:50:00 +0000"/>

<unit_system data="US"/>

</forecast_information>

<current_conditions>

<condition data="Clear"/>

<temp_f data="64"/>

<temp_c data="18"/>

<humidity data="Humidity: 40%"/>

<icon data="/ig/images/weather/sunny.gif"/>

<wind_condition data="Wind: W at 17 mph"/>

</current_conditions>

<forecast_conditions>

<day_of_week data="Sat"/>

<low data="55"/>

<high data="71"/>

<icon data="/ig/images/weather/chance_of_rain.gif"/>

<condition data="Chance of Rain"/>

</forecast_conditions>

<forecast_conditions>

<day_of_week data="Sun"/>

<low data="64"/>

<high data="69"/>

<icon data="/ig/images/weather/chance_of_rain.gif"/>

<condition data="Chance of Rain"/>

</forecast_conditions>

<forecast_conditions>

<day_of_week data="Mon"/>

<low data="62"/>

<high data="77"/>

<icon data="/ig/images/weather/chance_of_rain.gif"/>

<condition data="Chance of Rain"/>

</forecast_conditions>

<forecast_conditions>

<day_of_week data="Tue"/>

<low data="59"/>

<high data="73"/>

<icon data="/ig/images/weather/chance_of_rain.gif"/>

<condition data="Chance of Rain"/>

</forecast_conditions>

</weather>

</xml_api_reply>

1

As far as I can tell, this is unimplemented, and Google does not do reverse geocoding to populate the latitude and longitude fields of the XML document with the values for the town or city concerned.

If we make a request about a nonexistent location—for instance, http://www.google.com/ig/api?weather=Foo—we’ll get the following (rather unhelpful) XML error document returned:

<?xml version="1.0"?>

<xml_api_reply version="1">

<weather module_id="0" tab_id="0" mobile_row="0"

mobile_zipped="1" row="0" section="0" >

<problem_cause data=""/>1

</weather>

</xml_api_reply>

1

A far as I can tell this is unimplemented, and Google doesn’t populate this field.

Building an application

Much like Apple’s own Weather application, the application we’re going to wrap around the Google Weather Service will be a utility application. So, open Xcode and start a new project. Select the Utility Application template from the iPhone OS category, and name the project “Weather” when prompted for a filename.

Note

The UI for this application will be pretty complicated, and will have a lot more elements than interfaces we’ve looked at before. So, I’ll briefly mention an alternative. I could easily have implemented the Weather application as a table view; in fact, programmatically this is probably the easiest way, but it’s not the prettiest.

Pretty is important, both to people developing applications for Apple products and to the typical customer base. If you intend to sell your application on the App Store, you should think seriously about how your application looks. First impressions are important, and with so many applications available, both the UI and the application’s icon are tools you can use to make your application stand out from the others.

While we’re going to be spending some time putting together the interface for the application, that isn’t the main focus of this chapter. However, most of the time you’ll be using the NSURLConnection class asynchronously, so it’s important for you to pay attention to the way it fits into the UI and your application’s overall structure.

First we need to add a number of IBOutlets to our MainViewController.h interface file. We’re going to populate our GUI by querying the Google Weather Service and then parsing the XML we get back. If you compare the following to the XML file shown earlier, you should see a more or less one-to-one correspondence between XML elements and UI elements:

#import "FlipsideViewController.h"

@interface MainViewController : UIViewController

<FlipsideViewControllerDelegate> {

IBOutlet UIActivityIndicatorView *loadingActivityIndicator;

IBOutlet UILabel *nameLabel;

IBOutlet UILabel *dateLabel;

IBOutlet UIImageView *nowImage;

IBOutlet UILabel *nowTempLabel;

IBOutlet UILabel *nowHumidityLabel;

IBOutlet UILabel *nowWindLabel;

IBOutlet UILabel *nowConditionLabel;

IBOutlet UILabel *dayOneLabel;

IBOutlet UIImageView *dayOneImage;

IBOutlet UILabel *dayOneTempLabel;

IBOutlet UILabel *dayOneChanceLabel;

IBOutlet UILabel *dayTwoLabel;

IBOutlet UIImageView *dayTwoImage;

IBOutlet UILabel *dayTwoTempLabel;

IBOutlet UILabel *dayTwoChanceLabel;

IBOutlet UILabel *dayThreeLabel;

IBOutlet UIImageView *dayThreeImage;

IBOutlet UILabel *dayThreeTempLabel;

IBOutlet UILabel *dayThreeChanceLabel;

IBOutlet UILabel *dayFourLabel;

IBOutlet UIImageView *dayFourImage;

IBOutlet UILabel *dayFourTempLabel;

IBOutlet UILabel *dayFourChanceLabel;

IBOutlet UIButton *refreshButton;

}

- (IBAction)showInfo;

- (IBAction)refreshView:(id) sender;1

- (void)updateView;2

@end

1

This method is called when the user taps the Refresh button. It starts the loading activity indicator spinning, and makes the call to query the Google Weather Service.

2

We’re going to use this function as a callback when we have successfully retrieved the XML document from the Google Weather Service. It will update the current view.

Now let’s open MainView.xib in Interface Builder and put together the UI. I’m not going to walk you through the steps for building the interface this time. You’ve built enough UIs by this point that you should be familiar with how to go about it. Look at Figure 7-10 to see my final interface. You need to place 35 UI elements: 28 labels (UILabel), 5 images (UIImageView), 1 activity indicator (UIActivityIndicatorView), and 1 button (UIButton). However, don’t be put off; it’s really not going to take as long as you think it will.

Note

Remember: to change the font color, minimum size, and other settings, use the Attribute Inspector (⌘-1). You can change the attributes of several elements at once by dragging to select them, and then using the Attribute Inspector.

Building the UI for the main view

Figure 7-10. Building the UI for the main view

There are a few points to note:

§ Each UIImage element must be resized to 40×40 pixels, the size of the GIF weather icons provided by the Google Weather Service.

§ I set the style of the UIActivityIndicatorViewer to Large White in the Attributes Inspector and ticked the Hide When Stopped checkbox. We’ll use this indicator to show network or other activity.

§ I added a custom PNG icon for the Refresh button to the project, setting the UIButton type to Custom and the image to point at my refresh icon (you will need to drag your icon into your Xcode project before it will be available as a custom image). I resized the Refresh button to be the same size as the Info button provided by the template, setting the View Mode to “Scale to Fill” in the Attributes tab of the Inspector window.

§ When connecting the UIButtons to the received actions—for example, when dragging the refreshView: action to the Refresh button—choose Touch Up Inside from the drop-down menu of events that Interface Builder will present to you when you make the connection.

With this number of UI elements to play with, it’s going to be easy to get confused. What’s more, we are not going to connect all of the labels to our code, as some of them aren’t going to be updated (e.g., section headers and the “Temp:”, “Humidity:”, and “Wind:” labels).

So, for the elements you will connect to an IBOutlet, use the Identity Inspector’s (⌘-4) Interface Builder Identity section to change the Name attribute of the element to be the same as the variable in the MainViewController interface file. Figure 7-11 shows the assignments.

While this doesn’t make it easier to connect the outlets to the UI elements, it does make it easier to check whether we’ve made an incorrect connection. If you click on File’s Owner and switch to the Connections tab of the Inspector window, as Figure 7-12 shows, you can quickly check that each outlet is connected to the correct UI element since the name on each side of the connection should be the same.

Although we’ve written the interface for the view controller and built and connected our view to the interface, we haven’t implemented it yet. Let’s hold off on that until we’ve built our data model.

Associating names and variables with individual UI elements

Figure 7-11. Associating names and variables with individual UI elements

Connecting the IBOutlets declared in the MainViewController.h file to the appropriate UI elements

Figure 7-12. Connecting the IBOutlets declared in the MainViewController.h file to the appropriate UI elements

Our model class needs to query the weather service, parse the response, and populate the data model. Right-click on the Other Sources group in the Groups & Files pane in Xcode and select Add→New File, select the Objective-C class from the iPhone OS Cocoa Touch category, and select NSObject from the “Subclass of” pop up. Click Next. Name the new class WeatherForecast when prompted, and open the WeatherForecast.h interface file in the Xcode editor. Like our UI, the interface file reflects the structure of the XML document we retrieved from the Google Weather Service. Add the lines shown in bold to the file:

#import <Foundation/Foundation.h>

@class MainViewController;

@interface WeatherForecast : NSObject {

// Parent View Controller1

MainViewController *viewController;

// Google Weather Service2

NSMutableData *responseData;

NSURL *theURL;

// Information3

NSString *location;

NSString *date;

// Current Conditions4

UIImage *icon;

NSString *temp;

NSString *humidity;

NSString *wind;

NSString *condition;

// Forecast Conditions5

NSMutableArray *days;

NSMutableArray *icons;

NSMutableArray *temps;

NSMutableArray *conditions;

}

@property (nonatomic, retain) NSString *location;

@property (nonatomic, retain) NSString *date;

@property (nonatomic, retain) UIImage *icon;

@property (nonatomic, retain) NSString *temp;

@property (nonatomic, retain) NSString *humidity;

@property (nonatomic, retain) NSString *wind;

@property (nonatomic, retain) NSString *condition;

@property (nonatomic, retain) NSMutableArray *days;

@property (nonatomic, retain) NSMutableArray *icons;

@property (nonatomic, retain) NSMutableArray *temps;

@property (nonatomic, retain) NSMutableArray *conditions;

- (void)queryService:(NSString *)city

withParent:(UIViewController *)controller;6

@end

1

This is the variable used to hold the parent view controller. We’re going to pass this in to the Forecast object when we call the queryService:withParent method.

2

These are the variables used by the NSURLConnection class during its asynchronous request.

3

These are the variables to hold the data from the <forecast_information> XML elements.

4

These are the variables to hold the data from the <current_conditions> XML elements.

5

These are the arrays to hold the data from the four <forecast_conditions> XML elements.

6

This is the method we’re going to use to trigger the asynchronous NSURLConnection request. We pass as arguments the name of the city we’re interested in and the parent view controller. This allows us to substitute the city name into a partially formed REST request to the Google Weather Service.

Now open the implementation file (WeatherForecast.m) in the Xcode editor. We need to synthesize our properties and write our queryService:withParent: method that will start the asynchronous NSURLConnection process. Add the lines shown in bold to this file:

#import "WeatherForecast.h"

#import "MainViewController.h"

@implementation WeatherForecast

@synthesize location;

@synthesize date;

@synthesize icon;

@synthesize temp;

@synthesize humidity;

@synthesize wind;

@synthesize condition;

@synthesize days;

@synthesize icons;

@synthesize temps;

@synthesize conditions;

#pragma mark Instance Methods

- (void)queryService:(NSString *)city

withParent:(UIViewController *)controller

{

viewController = (MainViewController *)controller;

responseData = [[NSMutableData data] retain];

NSString *url = [NSString

stringWithFormat:@"http://www.google.com/ig/api?weather=%@",

city];1

theURL = [[NSURL URLWithString:url] retain];

NSURLRequest *request = [NSURLRequest requestWithURL:theURL];2

[[NSURLConnection alloc] initWithRequest:request delegate:self];3

}

-(void)dealloc {

[viewController release];

[responseData release];

[theURL release];

[location release];

[date release];

[icon release];

[temp release];

[humidity release];

[wind release];

[condition release];

[days release];

[icons release];

[temps release];

[conditions release];

[super dealloc];

}

@end

1

This builds the URL from the base URL and the city string that was passed to the queryService:withParent: method.

2

This builds the NSURLRequest from the URL string.

3

This makes the call to the Google Weather Service using an asynchronous call to NSURLConnection.

We declared our WeatherForecast class as the delegate for the NSURLConnection class. Now we need to add the necessary delegate methods. For now let’s just implement the delegate methods; we’ll get around to parsing the response later. Add the following lines toWeatherForecast.m just before the @end directive:

#pragma mark NSURLConnection Delegate Methods

- (NSURLRequest *)connection:(NSURLConnection *)connection

willSendRequest:(NSURLRequest *)request

redirectResponse:(NSURLResponse *)redirectResponse

{

[theURL autorelease];

theURL = [[request URL] retain];

return request;

}

- (void)connection:(NSURLConnection *)connection

didReceiveResponse:(NSURLResponse *)response

{

[responseData setLength:0];

}

- (void)connection:(NSURLConnection *)connection

didReceiveData:(NSData *)data

{

[responseData appendData:data];

}

- (void)connection:(NSURLConnection *)connection

didFailWithError:(NSError *)error

{

}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection {

NSString *content = [[NSString alloc]

initWithBytes:[responseData bytes]

length:[responseData length]

encoding:NSUTF8StringEncoding];1

NSLog( @"Data = %@", content );2

// Insert code to parse the content here

[viewController updateView];3

}

1

This converts the binary response data into an NSString object.

2

Here we print the response to the console log.

3

This is where we call the updateView: method in our parent view controller to take the parsed response and display it in the view.

We’re going to use the application delegate to create the WeatherForecast object and to pass it to our MainViewController object. Add the lines shown in bold to WeatherAppDelegate.m:

- (void)applicationDidFinishLaunching:(UIApplication *)application {

MainViewController *aController =

[[MainViewController alloc] initWithNibName:@"MainView" bundle:nil];

self.mainViewController = aController;

[aController release];

WeatherForecast *forecast = [[WeatherForecast alloc] init];1

self.mainViewController.forecast = forecast;2

[forecast release];

mainViewController.view.frame = [UIScreen mainScreen].applicationFrame;

[window addSubview:[mainViewController view]];

[window makeKeyAndVisible];

}

1

We’re creating an instance of this class, which we’re going to store inside the app delegate. You’ll need to also add #import "WeatherForecast.h" to the top of WeatherAppDelegate.m.

2

We pass the forecast object to the view controller, and then release it in the app delegate. There is no need to store an instance here, as we won’t be using it from the delegate.

We have the view, model, and interface for the view controller. Now we know how the model works, and how we’re going to push it into the view controller. So, let’s implement the controller and tie up those loose ends. Add the following code to MainViewController.m:

- (void)viewDidLoad {1

[super viewDidLoad];

[self refreshView:self];

}

- (IBAction)refreshView:(id)sender {2

[loadingActivityIndicator startAnimating];

[forecast queryService:@"London,UK" withParent:self];

}

- (void)updateView {3

// Add code to update view here

[loadingActivityIndicator stopAnimating];

}

1

This is called when the view loads. This calls the viewDidLoad: method in the superclass and then calls the refreshView: method.

2

This method is called when the Refresh button is tapped, and also from the viewDidLoad: method. This starts the UIActivityViewIndicator spinning and then calls the queryService:withParent: method in the WeatherForecast object.

3

This method is called from the WeatherForecast object when it finishes loading the XML from the remote service. This method will contain the code to update the view using the newly populated WeatherForecast object. For now all it does is stop the UIActivityView from spinning and hides it.

Additionally, we also need to make sure we do the following:

1. Import the WeatherForecast.h interface file inside MainViewController.h.

2. Declare the forecast, mark it as a property, and synthesize it.

3. Release all of the variables we declared in the class’s interface file in MainViewController.m’s dealloc: method.

To do this, add the following line to the top of MainViewController.h:

#import "WeatherForecast.h"

Next, make the changes shown in bold to the end of MainViewController.h:

IBOutlet UIButton *refreshButton;

WeatherForecast *forecast;

}

- (IBAction)showInfo;

- (IBAction)refreshView:(id) sender;

- (void)updateView;

@property (nonatomic, retain) WeatherForecast *forecast;

@end

Then make the change shown in bold to the top of MainViewController.h:

#import "MainViewController.h"

#import "MainView.h"

@implementation MainViewController

@synthesize forecast;

Finally, add the lines shown in bold to MainViewController.m’s dealloc: method:

- (void)dealloc {

[loadingActivityIndicator dealloc];

[nameLabel dealloc];

[dateLabel dealloc];

[nowImage dealloc];

[nowTempLabel dealloc];

[nowHumidityLabel dealloc];

[nowWindLabel dealloc];

[nowConditionLabel dealloc];

[dayOneLabel dealloc];

[dayOneImage dealloc];

[dayOneTempLabel dealloc];

[dayOneChanceLabel dealloc];

[dayTwoLabel dealloc];

[dayTwoImage dealloc];

[dayTwoTempLabel dealloc];

[dayTwoChanceLabel dealloc];

[dayThreeLabel dealloc];

[dayThreeImage dealloc];

[dayThreeTempLabel dealloc];

[dayThreeChanceLabel dealloc];

[dayFourLabel dealloc];

[dayFourImage dealloc];

[dayFourTempLabel dealloc];

[dayFourChanceLabel dealloc];

[refreshButton dealloc];

[forecast dealloc];

[super dealloc];

}

This is a good point to pause, take stock, and test the application. Click the Build and Run button in the Xcode toolbar. When the application opens you should see the UIActivityIndicator briefly appear in the top-lefthand corner of the view, and then disappear when theWeatherForecast object finishes loading the XML document from the Google Weather Service.

If you go to the Xcode Console, by selecting Run→Console from the Xcode menu bar, you should see something very much like Figure 7-13. This is the XML document retrieved from the weather service.

The Xcode Console window showing the XML retrieved from the Google Weather Service

Figure 7-13. The Xcode Console window showing the XML retrieved from the Google Weather Service

At this point, all that is left to implement is the XML parsing code inside the WeatherForecast’s connectionDidFinishLoading: method, and the code to take the data model from the forecast object and display it in the view inside the MainViewController’s updateView:method.

Parsing the XML document

We’re going to talk in detail about parsing data in the next chapter. This chapter is about networking, so I’m not going to discuss in depth how to parse the returned XML document here. If you’re familiar with DOM-based XML parsers, the following should be familiar. If not, hang on until the next chapter.

Warning

Making use of the NSXMLDocument class is the normal method for tree-based parsing of XML files on the Mac. However, despite being available in iPhone Simulator, this class is not available on the device itself.

However, for simple files, such as those returned by the Google Weather Service, I’ve never been a big fan of event-driven parsing. Since the NSXMLDocument class is not available on the iPhone, I generally use the libxml2 library directly, via Matt Gallagher’s excellent XPath wrappers for the library.

For more information about the XPath wrappers for the libxml2 library, see Matt’s blog post.

Using the XPath Wrappers

Download the wrappers from http://cocoawithlove.googlepages.com/XPathQuery.zip. Next, unzip the file and drag the XPathQuery.h and XPathQuery.m files into your project, remembering to tick the “Copy items into destination group’s folder” checkbox. This will add the interface and implementation files for the wrappers to the project.

To use these wrappers, you need to add the libxml2.dylib library to the project. However, adding the libxml2 library underlying these wrappers is slightly more involved than adding a normal framework:

1. Double-click on the Weather project icon in the Groups & Files pane in Xcode and go to the Build tab of the Project Info window.

2. Click on the Show drop-down menu and choose All Settings.

3. Go to the Search Paths subsection in this window, and in the Header Search Paths field double-click on the entry field.

4. Click the + button and add ${SDKROOT}/usr/include/libxml2 to the paths, as shown in Figure 7-14. Then click OK.

5. Then in the Linking subsection of this window, double-click on the Other Linker Flags field and click +. Add -lxml2 to the flags and then click OK.

Adding the libxml2.dylib library in the Project Info window

Figure 7-14. Adding the libxml2.dylib library in the Project Info window

Once we’ve done that, we can open the WeatherForecast.m implementation file and import the XPathQuery.h interface file. Add the following line to the top of WeatherForecast.m:

#import "XPathQuery.h"

After importing the interface file, we now have everything in place to write our connectionDidFinishLoading: method, using the XPath query language and libxml2 to parse the XML document returned by the Google Weather Service. My intention here is not to teach you XPath, as several good books are available on that topic. However, if you examine the xpathQueryString variables in each XPath query, you will see how the data model maps onto the original XML document returned by the weather service. Here is the new connectionDidFinishLoading:method along with two methods (fetchContent: and populateArray:fromNodes:) to take care of some repetitive tasks:

// Retrieves the content of an XML node, such as the temperature, wind,

// or humidity in the weather report.

//

- (NSString *)fetchContent:(NSArray *)nodes {

NSString *result = @"";

for ( NSDictionary *node in nodes ) {

for ( id key in node ) {

if( [key isEqualToString:@"nodeContent"] ) {

result = [node objectForKey:key];

}

}

}

return result;

}

// For nodes that contain more than one value we are interested in,

// this method fills an NSMutableArray with the values it finds.

// For example, the forecast returns four days, so there will be

// an array with four day names, an array with four forecast icons,

// and so forth.

//

- (void) populateArray:(NSMutableArray *)array fromNodes:(NSArray*)nodes {

for ( NSDictionary *node in nodes ) {

for ( id key in node ) {

if( [key isEqualToString:@"nodeContent"] ) {

[array addObject:[node objectForKey:key]];

}

}

}

}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection {

days = [[NSMutableArray alloc] init];

icons = [[NSMutableArray alloc] init];

temps = [[NSMutableArray alloc] init];

conditions = [[NSMutableArray alloc] init];

NSString *xpathQueryString;

NSArray *nodes;

// Forecast Information ////////////////////////////////////////

// Populate the location (an NSString object)

//

xpathQueryString = @"//forecast_information/city/@data";

nodes = PerformXMLXPathQuery(responseData, xpathQueryString);

location = [self fetchContent:nodes];

NSLog(@"location = %@", location);

// Populate the date (an NSString object)

//

xpathQueryString = @"//forecast_information/forecast_date/@data";

nodes = PerformXMLXPathQuery(responseData, xpathQueryString);

date = [self fetchContent:nodes];

NSLog(@"date = %@", date);

// Current Conditions ////////////////////////////////////////

// Populate the current day's weather icon (a UIImage object)

//

xpathQueryString = @"//current_conditions/icon/@data";

nodes = PerformXMLXPathQuery(responseData, xpathQueryString);

for ( NSDictionary *node in nodes ) {

for ( id key in node ) {

if( [key isEqualToString:@"nodeContent"] ) {

icon = [NSString

stringWithFormat:@"http://www.google.com%@",

[node objectForKey:key]];

}

}

}

NSLog(@"icon = %@", icon);

// Populate the temperature (an NSString object) in F and C

//

NSString *temp_f;

NSString *temp_c;

xpathQueryString = @"//current_conditions/temp_f/@data";

nodes = PerformXMLXPathQuery(responseData, xpathQueryString);

temp_f = [self fetchContent:nodes];

xpathQueryString = @"//current_conditions/temp_c/@data";

nodes = PerformXMLXPathQuery(responseData, xpathQueryString);

temp_c = [self fetchContent:nodes];

temp = [NSString stringWithFormat:@"%@F (%@C)", temp_f, temp_c];

NSLog(@"temp = %@", temp);

// Populate the humidity (an NSString object)

//

xpathQueryString = @"//current_conditions/humidity/@data";

nodes = PerformXMLXPathQuery(responseData, xpathQueryString);

humidity = [self fetchContent:nodes];

NSLog(@"humidity = %@", humidity);

// Populate the wind (an NSString object)

//

xpathQueryString = @"//current_conditions/wind_condition/@data";

nodes = PerformXMLXPathQuery(responseData, xpathQueryString);

wind = [self fetchContent:nodes];

NSLog(@"wind = %@", wind);

// Populate the condition (an NSString object)

//

xpathQueryString = @"//current_conditions/condition/@data";

nodes = PerformXMLXPathQuery(responseData, xpathQueryString);

condition = [self fetchContent:nodes];

NSLog(@"condition = %@", condition);

// Forecast Conditions ////////////////////////////////////////

// Fill the array (an NSMutableArray) of day names

//

xpathQueryString = @"//forecast_conditions/day_of_week/@data";

nodes = PerformXMLXPathQuery(responseData, xpathQueryString);

[self populateArray:days fromNodes:nodes];

NSLog(@"days = %@", days);

// Fill the array (an NSMutableArray) of day icons

//

xpathQueryString = @"//forecast_conditions/icon/@data";

nodes = PerformXMLXPathQuery(responseData, xpathQueryString);

for ( NSDictionary *node in nodes ) {

for ( id key in node ) {

if( [key isEqualToString:@"nodeContent"] ) {

[icons addObject:

[NSString stringWithFormat:@"http://www.google.com%@",

[node objectForKey:key]]];

}

}

}

NSLog(@"icons = %@", icons);

// Fill the array (an NSMutableArray) of daily highs/lows

//

NSMutableArray *highs = [[NSMutableArray alloc] init];

NSMutableArray *lows = [[NSMutableArray alloc] init];

xpathQueryString = @"//forecast_conditions/high/@data";

nodes = PerformXMLXPathQuery(responseData, xpathQueryString);

[self populateArray:highs fromNodes:nodes];

xpathQueryString = @"//forecast_conditions/low/@data";

nodes = PerformXMLXPathQuery(responseData, xpathQueryString);

[self populateArray:lows fromNodes:nodes];

for( int i = 0; i < highs.count; i++ ) {

[temps

addObject:[NSString stringWithFormat:@"%@F/%@F",

[highs objectAtIndex:i],

[lows objectAtIndex:i]]];

}

NSLog(@"temps = %@", temps);

[highs release];

[lows release];

// Fill the array (an NSMutableArray) of daily conditions

//

xpathQueryString = @"//forecast_conditions/condition/@data";

nodes = PerformXMLXPathQuery(responseData, xpathQueryString);

[self populateArray:conditions fromNodes:nodes];

NSLog(@"conditions = %@", conditions);

[viewController updateView];

}

Populating the UI

Now that we’ve populated the data model, let’s create the updateView: method in our view controller. This is where we take the data that we just parsed from the XML and push it into the current view. Replace the updateView: method in MainViewController.m with the following:

- (void)updateView {

// City Info

nameLabel.text = forecast.location;

dateLabel.text = forecast.date;

// Now

nowTempLabel.text = forecast.temp;

nowHumidityLabel.text = forecast.humidity;

nowWindLabel.text = forecast.wind;

nowConditionLabel.text = forecast.condition;

NSURL *url = [NSURL URLWithString:(NSString *)forecast.icon];

NSData *data = [NSData dataWithContentsOfURL:url];

nowImage.image = [[UIImage alloc] initWithData:data];

// Day 1

dayOneLabel.text = [forecast.days objectAtIndex:0];

dayOneTempLabel.text = [forecast.temps objectAtIndex:0];

dayOneChanceLabel.text = [forecast.conditions objectAtIndex:0];

url = [NSURL URLWithString:(NSString *)[forecast.icons objectAtIndex:0]];

data = [NSData dataWithContentsOfURL:url];

dayOneImage.image = [[UIImage alloc] initWithData:data];

// Day 2

dayTwoLabel.text = [forecast.days objectAtIndex:1];

dayTwoTempLabel.text = [forecast.temps objectAtIndex:1];

dayTwoChanceLabel.text = [forecast.conditions objectAtIndex:1];

url = [NSURL URLWithString:(NSString *)[forecast.icons objectAtIndex:1]];

data = [NSData dataWithContentsOfURL:url];

dayTwoImage.image = [[UIImage alloc] initWithData:data];

// Day 3

dayThreeLabel.text = [forecast.days objectAtIndex:2];

dayThreeTempLabel.text = [forecast.temps objectAtIndex:2];

dayThreeChanceLabel.text = [forecast.conditions objectAtIndex:2];

url = [NSURL URLWithString:(NSString *)[forecast.icons objectAtIndex:2]];

data = [NSData dataWithContentsOfURL:url];

dayThreeImage.image = [[UIImage alloc] initWithData:data];

// Day 4

dayFourLabel.text = [forecast.days objectAtIndex:3];

dayFourTempLabel.text = [forecast.temps objectAtIndex:3];

dayFourChanceLabel.text = [forecast.conditions objectAtIndex:3];

url = [NSURL URLWithString:(NSString *)[forecast.icons objectAtIndex:3]];

data = [NSData dataWithContentsOfURL:url];

dayFourImage.image = [[UIImage alloc] initWithData:data];

[loadingActivityIndicator stopAnimating];

}

We’re done. Click the Build and Run button on the Xcode toolbar to build and start the application in iPhone Simulator.

Once the application starts up, if all goes well you should get something that looks similar to Figure 7-15. There is, after all, almost always a chance of rain in London.

The Weather application running in iPhone Simulator

Figure 7-15. The Weather application running in iPhone Simulator

Tidying up

There are several things you can do to tidy up this bare-bones application. First you should clean up the UI, as it’s pretty untidy when the application opens. The easiest way to do this is to have all your labels start as blank, and then populate the text when the view finishes loading the information from the Google Weather Service.

You might also want to add reachability checks when the application opens, and add some error handling in the connection:didFailWithError: delegate method inside the WeatherForecast class. You should also allow the user to choose which city to use by adding a text entry box on the flipside view, or perhaps even a map view.

In Chapter 11, we’ll come back to this example when we discuss using device sensors. Most people are usually more concerned with the weather where they are now than the weather somewhere else, so we’ll use the Core Location framework and the iPhone’s GPS to locate users and provide them with a weather forecast for where they are right now.