Handling Data - Learning iPhone Programming (2010)

Learning iPhone Programming (2010)

Chapter 8. Handling Data

Most applications on the iPhone platform will make a network connection to retrieve data at some point. This data will usually be formatted so that it can be easily parsed, either as XML or, more frequently these days, as JSON.

In this chapter, we’re going to look at how to get data directly from the user via the UI, and then how to parse data we’ve retrieved from the network. Finally, we’ll look at how to store that data on the device.

Data Entry

The Cocoa Touch framework offers a number of UI elements, ranging from text entry fields to switches and segmented controls. Any of these can be used for data entry, but often when we talk about data entry we’re talking about getting textual information into an application.

The two main UI elements that allow you to enter text are the UITextField and UITextView classes. While they may sound similar, they are actually quite different. The most noticeable difference between the two is that the UITextView allows you to enter (and display) a multiline text field, while UITextField doesn’t.

The most annoying difference between the two is the issue of the resigning first responder. When tapped, both display a keyboard to allow the user to enter text. However, while the UITextField class allows the user to dismiss the keyboard (at which time the text field resigns as first responder) when the user taps the Done button, the UITextView class does not. Though there are multiple ways around this problem, as we’ll find later on, it’s still one of the more annoying quirks in the Cocoa Touch framework.

UITextField and Its Delegate

In Chapter 5, we used a UITextField as part of our AddCityController view. However, we didn’t really exploit the full power of this class. We were simply polling the text field to see if the user had entered any text when the Save button was tapped, and perhaps more important, we weren’t dismissing the keyboard when the user pressed the Return key. Here’s the saveCity:sender method from that example:

- (void)saveCity:(id)sender {

CityGuideDelegate *delegate =

(CityGuideDelegate *)[[UIApplication sharedApplication] delegate];

NSMutableArray *cities = delegate.cities;

UITextField *nameEntry = (UITextField *)[nameCell viewWithTag:777];

UITextView *descriptionEntry =

(UITextView *)[descriptionCell viewWithTag:777];

if ( nameEntry.text.length > 0 ) {

City *newCity = [[City alloc] init];

newCity.cityName = nameEntry.text;

newCity.cityDescription = descriptionEntry.text;

newCity.cityPicture = cityPicture;

[cities addObject:newCity];

RootController *viewController = delegate.viewController;

[viewController.tableView reloadData];

}

[delegate.navController popViewControllerAnimated:YES];

}

However, the UITextFieldDelegate protocol offers a rich set of delegate methods. To use them, you must declare your class as implementing that delegate protocol (lines with changes are shown in bold):

@interface AddCityController : UIViewController

<UITableViewDataSource, UITableViewDelegate, UITextFieldDelegate>

{

UITextField *activeTextField; 1

... remainder of example code not shown ...

}

1

If your application has more than one text field in the view, it’s useful to keep track of which is currently the active field by using an instance variable.

Note

After implementing the delegate protocol, open the NIB that contains the UITextField (AddCityController.xib in the case of CityGuide). Next, Ctrl-drag from the UITextField to the controller (File’s Owner in AddCityController.xib) and select delegates from the pop up that appears. Save the NIB when you’re done.

When the user taps the text field, the textFieldShouldBeginEditing: method is called in the delegate to ascertain whether the text field should enter edit mode and become the first responder. To implement this, you’d add the following to your controller’s implementation (such asAddCityController.m):

- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField {

activeTextField = textField;1

return YES;

}

1

If your application has more than one text field in the view, here’s where you’d set the currently active field.

If this method returns NO, the text field will not become editable. Only if this method returns YES will the text field enter edit mode. At this point, the keyboard will be presented to the user; the text field will become the first responder; and the textFieldDidBeginEditing: delegate method will be called.

The easiest way to hide the keyboard is to implement the textFieldShouldReturn: delegate method and explicitly resign as the first responder. This method is called in the delegate when the Return key on the keyboard is pressed. To dismiss the text field when you tapped on the Return button, you’d add the following to your controller’s implementation:

- (BOOL)textFieldShouldReturn:(UITextField *)textField {

activeTextField = nil;1

[textField resignFirstResponder];

return YES;

}

1

If your application is keeping track of the currently active text field, this is where you should set the active field to nil before it resigns as first responder.

This method is usually used to make the text field resign as first responder, at which point the delegate methods textFieldShouldEndEditing: and textFieldDidEndEditing: will be triggered.

These methods can be used to update the data model with new content if required, or after parsing the input, to make other appropriate changes to the UI such as adding or removing additional elements.

UITextView and Its Delegate

As with the UITextField we used as part of our AddCityController view in Chapter 5, we didn’t exploit the full power of the UITextView class in that example. Like the UITextField, the UITextView class has an associated delegate protocol that opens up its many capabilities.

Dismissing the UITextView

The UITextViewDelegate protocol lacks the equivalent to the textFieldShouldReturn: method, presumably since we shouldn’t expect the Return key to be a signal that the user wishes to stop editing the text in a multiline text entry dialog (after all, the user may want to insert line breaks by pressing Return).

However, there are several ways around the inability of the UITextView to resign as first responder using the keyboard. The usual method is to place a Done button in the navigation bar when the UITextView presents the pop-up keyboard. When tapped, this button asks the text view to resign as first responder, which will then dismiss the keyboard.

However, depending on how you’ve planned out your interface, you might want the UITextView to resign when the user taps outside the UITextView itself.

To do this, you’d subclass UIView to accept touches, and then instruct the text view to resign when the user taps outside the view itself. Right-click on the Classes group in the Groups & Files pane in the Xcode interface, select Add→New File, and choose Cocoa Touch Class from the iPhone OS section. Next, select “Objective-C class” and choose UIView from the “Subclass of” menu. Click Next and name the class “CustomView”.

In the interface (CustomView.h), add an IBOutlet for a UITextView:

#import <UIKit/UIKit.h>

@interface CustomView : UIView {

IBOutlet UITextView *textView;

}

@end

Then, in the implementation (CustomView.m), implement the touchesEnded:withE⁠vent: method and ask the UITextView to resign as first responder. Here’s what the implementation should look like (added lines are shown in bold):

#import "CustomView.h"

@implementation CustomView

- (id)initWithFrame:(CGRect)frame {

if (self = [super initWithFrame:frame]) {

// Initialization code

}

return self;

}

- (void)dealloc {

[super dealloc];

}

- (void) awakeFromNib {

self.multipleTouchEnabled = YES;

}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {

NSLog(@"touches began count %d, %@", [touches count], touches);

[textView resignFirstResponder];

[self.nextResponder touchesEnded:touches withEvent:event];

}

@end

Once you’ve added the class, you need to save all your changes, then go into Interface Builder and click on your view. Open the Identity Inspector (⌘-4) and change the type of the view in your NIB file to be your CustomView rather than the default UIView class. Then in the Connections Inspector (⌘-2), drag the textView outlet to the UITextView. After doing so, and once you rebuild your application, touches outside the active UI elements will now dismiss the keyboard.

Warning

If the UIView you are subclassing is “behind” other UI elements, these elements will intercept the touches before they reach the UIView layer.

For instance, in the case of the CityGuide3 application from Chapter 6 and its Add City interface, you would have to declare your custom view to be a subclass of the UITableViewCell class rather than a UIView. You would then need to change the class of the three table view cells in the AddCityController.xib main window to be CustomView rather than the default UITableViewCell (don’t change the class of the view).

You’d then need to connect the textView outlet of all three table view cells to the UITextView in the table view cell used to enter the long description.

While this solution is elegant, it can be used in only some situations. In many cases, you’ll have to resort to the brute force method of adding a Done button to the navigation bar to dismiss the keyboard.

Parsing XML

The two widely used methods for parsing an XML document are SAX and DOM. A SAX (Simple API for XML) parser is event-driven. It reads the XML document incrementally and calls a delegate method whenever it recognizes a token. Events are generated at the beginning and end of the document, and the beginning and end of each element. A DOM (Document Object Model) parser reads the entire document and forms a tree-like corresponding structure in memory. You can then use the XPath query language to select individual nodes of the XML document using a variety of criteria.

Most programmers find the DOM method more familiar and easier to use; however, SAX-based applications are generally more efficient, run faster, and use less memory. So, unless you are constrained by system requirements, the only real factor when deciding to use SAX or DOM parsers comes down to preference.

If you want to know more about XML, I recommend Learning XML, Second Edition by Erik T. Ray (O’Reilly) as a good place to start.

Parsing XML with libxml2

We met the libxml2 parser and Matt Gallagher’s XPath wrappers in the preceding chapter, and my advice is to use these wrappers if you want to do DOM-based parsing of XML on the iPhone or iPod touch.

See the sidebar Using the XPath Wrappers in Chapter 7 for instructions on adding the XPathQuery wrappers to your project.

The wrappers offer two methods. The only difference between the two is that one expects an HTML document and is therefore less strict about what constitutes a “proper” document than the other, which expects a valid XML document:

NSArray *PerformHTMLXPathQuery(NSData *document, NSString *query);

NSArray *PerformXMLXPathQuery(NSData *document, NSString *query);

If you want to return the entire document as a single data structure, the following will do that. Be warned that except for the simplest of XML documents, this will normally generate a heavily nested structure of array and dictionary elements, which isn’t particularly useful:

NSString *xpathQueryString;

NSArray *nodes;

xpathQueryString = @"/*";

nodes = PerformXMLXPathQuery(responseData, xpathQueryString);

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

Let’s take a quick look at the XML document returned by the Google Weather Service that we parsed in Chapter 7’s Weather application. The XML document had a structure that looked like the following snippet:

<forecast_conditions>

...

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

</forecast_conditions>

<forecast_conditions>

...

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

</forecast_conditions>

<forecast_conditions>

...

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

</forecast_conditions>

<forecast_conditions>

...

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

</forecast_conditions>

To extract the URL of the icons, we carried out an XPath query:

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

nodes = PerformXMLXPathQuery(responseData, xpathQueryString);

1

Here we’re looking for the data attributes as part of an <icon> element, nested inside a <forecast_conditions> element. An array of all such occurrences will be returned.

The nodes array returned by the PerformXMLXPathQuery method looked like this:

( {

nodeContent = "/ig/images/weather/mostly_sunny.gif";

nodeName = data;

},

{

nodeContent = "/ig/images/weather/chance_of_rain.gif";

nodeName = data;

},

{

nodeContent = "/ig/images/weather/mostly_sunny.gif";

nodeName = data;

},

{

nodeContent = "/ig/images/weather/mostly_sunny.gif";

nodeName = data;

}

)

This structure is an NSArray of NSDictionary objects, and we parsed this by iterating through each array entry and extracting the dictionary value for the key nodeContent, adding each occurrence to the icons array:

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]]];

}

}

}

Parsing XML with NSXMLParser

The official way to parse XML on the iPhone is to use the SAX-based NSXMLParser class. However, the parser is strict and cannot take HTML documents:

NSString *url = @"http://feeds.feedburner.com/oreilly/news";

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

NSXMLParser *parser = [[NSXMLParser alloc] initWithContentsOfURL:theURL];

[parser setDelegate:self];

[parser setShouldResolveExternalEntities:YES];

BOOL success = [parser parse];

NSLog(@"Success = %d", success);

We use the parser by passing it an XML document and then implementing its delegate methods. The NSXMLParser class offers the following delegate methods:

parserDidStartDocument:

parserDidEndDocument:

parser:didStartElement:namespaceURI:qualifiedName:attributes:

parser:didEndElement:namespaceURI:qualifiedName:

parser:didStartMappingPrefix:toURI:

parser:didEndMappingPrefix:

parser:resolveExternalEntityName:systemID:

parser:parseErrorOccurred:

parser:validationErrorOccurred:

parser:foundCharacters:

parser:foundIgnorableWhitespace:

parser:foundProcessingInstructionWithTarget:data:

parser:foundComment:

parser:foundCDATA:

The most heavily used delegate methods out of all of those available methods are the parser:didStartElement:namespaceURI:qualifiedName:attributes: method and the parser:didEndElement:namespaceURI:qualifiedName: method. These two methods, along with the parser:foundCharacters: method, will allow you to detect the start and end of a selected element and retrieve its contents. When the NSXMLParser object traverses an element in an XML document, it sends three separate messages to its delegate, in the following order:

parser:didStartElement:namespaceURI:qualifiedName:attributes:

parser:foundCharacters:

parser:didEndElement:namespaceURI:qualifiedName:

Returning to the Weather application: to replace our XPath- and DOM-based solution with an NSXMLParser-based solution, we would substitute the following code for the existing queryService:withParent: method:

- (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];

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

NSXMLParser *parser = [[NSXMLParser alloc] initWithContentsOfURL:theURL];

[parser setDelegate:self];

[parser setShouldResolveExternalEntities:YES];

BOOL success = [parser parse];

}

We would then need to delete all of the NSURLConnection delegate methods, substituting the following NSXMLParser delegate method to handle populating our arrays:

- (void)parser:(NSXMLParser *)parser

didStartElement:(NSString *)elementName

namespaceURI:(NSString *)namespaceURI

qualifiedName:(NSString *)qName

attributes:(NSDictionary *)attributeDict {

// Parsing code to retrieve icon path

if([elementName isEqualToString:@"icon"]) {

NSString *imagePath = [attributeDict objectForKey:@"data"];

[icons addObject:

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

}

// ... add remaining parsing code for other elements here

[viewController updateView];

}

Warning

This example parses only the icon element; if you wanted to use NSXMLParser here, you’d need to look at connectionDidFinishLoading: in the original Weather app, and add parsing code for each of those elements before you call [viewController updateView] in this method (otherwise, it will throw an exception and crash the app because none of the data structures are populated).

Unless you’re familiar with SAX-based parsers, I suggest that XPath and DOM are conceptually easier to deal with than the event-driven model of SAX. This is especially true if you’re dealing with HTML, as an HTML document would have to be cleaned up before being passed to theNSXMLParser class.

Parsing JSON

JSON is a lightweight data-interchange format, which is more or less human-readable but still easily machine-parsable. While XML is document-oriented, JSON is data-oriented. If you need to transmit a highly structured piece of data, you should probably render it in XML. However, if your data exchange needs are somewhat less demanding, JSON might be a good option.

The obvious advantage JSON has over XML is that since it is data-oriented and (almost) parsable as a hash map, there is no requirement for heavyweight parsing libraries. Additionally, JSON documents are much smaller than the equivalent XML documents. In bandwidth-limited situations, such as you might find on the iPhone, this can be important. JSON documents normally consume around half of the bandwidth as an equivalent XML document for transferring the same data.

While there is no native support for JSON in the Cocoa Touch framework, Stig Brautaset’s json-framework library implements both a JSON parser and a generator and can be integrated into your project fairly simply.

Consuming Ruby on Rails

If you are dealing exclusively with Rails-based services, the ObjectiveResource framework (see http://iphoneonrails.com/ for more details) is a port of the ActiveResource framework of Ruby on Rails. It provides a way to serialize Rails objects to and from Rails’ standard RESTful web services via either XML or JSON. ObjectiveResource adds methods to NSObject using the category extension mechanism, so any Objective-C class can be treated as a remote resource.

Download the disk image with the latest version of the json-framework library from http://code.google.com/p/json-framework/. Open the disk image and drag and drop the JSON folder into the Classes group in the Groups & Files pane of your project. Remember to tick the “Copy items into destination group’s folder” checkbox before adding the files. This will add the JSON source files to your project; you will still need to import the JSON.h file into your class to use it.

Linking to the JSON Framework

Since dynamic linking to third-party embedded frameworks is not allowed on the iPhone platform, copying the JSON source files into your project is probably the simplest way to make the parser available to your application. However, there is a slightly more elegant approach if you don’t want to add the entire JSON source tree to every project where you use it.

Open the Finder and create an SDKs subfolder inside your home directory’s Library folder, and copy the JSON folder located inside the SDKs folder in the disk image into the newly created ~/Library/SDKs directory.

Back in Xcode, open your project and double-click on the project icon at the top of the Groups & Files pane to open the Project Info window. In the Architectures section in the Build tab, double-click on the Additional SDKs field and add$HOME/Library/SDKs/JSON/${PLATFORM_NAME}.sdk to the list of additional SDKs in the pop-up window. Now go to the Linking subsection of the Build tab, double-click on the Other Linker Flags field, and add -ObjC -all_load -ljson to the flags using the pop-up window.

Now you just have to add the following inside your source file:

#import <JSON/JSON.h>

Note the use of angle brackets rather than double quotes around the imported header file, denoting that it is located in the standard include path rather than in your project.

The Twitter Search Service

To let you get familiar with the json-framework library, let’s implement a bare-bones application to retrieve the trending topics on Twitter by making use of their RESTful Search API.

Note

If you’re interested in the Twitter API, you should definitely look at Twitter’s documentation for more details regarding the available methods. However, if you’re serious about using the Twitter API, you should probably look into using the MGTwitterEngine library written by Matt Gemmell. You can download it from http://mattgemmell.com/source.

Making a request to the Twitter Search API of the form http://search.twitter.com/trends.json will return a JSON document containing the top 10 topics that are currently trending on Twitter. The response includes the time of the request, the name of each trend, and the URL to the Twitter Search results page for that topic:

{

"trends":[

{

"name":"#musicmonday",

"url":"http:\/\/search.twitter.com\/search?q=%23musicmonday"

},

{

"name":"Spotify",

"url":"http:\/\/search.twitter.com\/search?q=Spotify+OR+%23Spotify"

},

{

"name":"Happy Labor Day",

"url":"http:\/\/search.twitter.com\/search?q=%22Happy+Labor+Day%22"

},

{

"name":"District 9",

"url":"http:\/\/search.twitter.com\/search?q=%22District+9%22"

},

{

"name":"Goodnight",

"url":"http:\/\/search.twitter.com\/search?q=Goodnight"

},

{

"name":"Chris Evans",

"url":"http:\/\/search.twitter.com\/search?q=%22Chris+Evans%22"

},

{

"name":"iPhone",

"url":"http:\/\/search.twitter.com\/search?q=iPhone+OR+%23Iphone"

},

{

"name":"Jay-Z",

"url":"http:\/\/search.twitter.com\/search?q=Jay-Z"

},

{

"name":"Dual-Screen E-Reader",

"url":"http:\/\/search.twitter.com\/search?q=%22E-Reader%22"

},

{

"name":"Cadbury",

"url":"http:\/\/search.twitter.com\/search?q=Cadbury"

}

],

"as_of":"Mon, 07 Sep 2009 09:18:34 +0000"

}

The Twitter Trends Application

Open Xcode and start a new iPhone Application project. Select the View-based Application template, and name the project “TwitterTrends” when prompted for a filename.

We’re going to need the JSON parser, so drag and drop the JSON source folder into the Classes group in the Groups & Files pane of your new project. Since the returned JSON document provides a URL, we’re also going to reuse the WebViewController class we wrote in Chapter 7. Open the Prototype project from Chapter 7, and drag and drop the WebViewController.m, WebViewController.h, and WebView.xib files from there into your new project.

Note

Remember to select the “Copy items into destination group’s folder” checkbox in the pop-up window when copying the files in both cases.

Refactoring

While we’re here, let’s do some refactoring. Open the TwitterTrendsAppDelegate.h file, right-click on the TwitterTrendsAppDelegate class name in the interface declaration, and select Refactor. This will bring up the Refactoring window. Let’s change the name of the main application delegate class from TwitterTrendsAppDelegate to TrendsDelegate. Entering the new class name and clicking Preview shows that three files will be affected by the change. Click Apply and Xcode will propagate changes throughout the project. Remember to save all the affected files (⌘-Option-S) before you go on to refactor the next set of classes.

Next, let’s refactor the TwitterTrendsViewController class, changing the class name from TwitterTrendsViewController to the more sensible RootController.

Open the TwitterTrendsViewController.h file, right-click on the TwitterTrendsViewController class name, and choose Refactor. Set the name to RootController. Click Preview, then Apply, and the changes will again propagate throughout the project. However, you’ll notice that Xcode has not changed the TwitterTrendsViewController.xib file to be more sensibly named, so you’ll have to make this change by hand. Click once on this file in the Groups & Files pane, wait a second, and click again; on the second click you’ll be able to rename it. Change its name to “RootView.xib”.

Unfortunately, since we had to make this change by hand, it hasn’t been propagated throughout the project. We’ll have to make some more manual changes. Double-click the MainWindow.xib file to open it in Interface Builder. Click on the Root Controller icon in the main NIB window and open the Attributes Inspector (⌘-1). The NIB name associated with the root controller is still set to TwitterTrendsViewController, so set this to RootView. You can either type the name of the controller into the window and Xcode will automatically carry out name completion as you type, or use the control on the right of the text entry box to get a drop-down panel where you’ll find the RootView NIB listed. Save and close the MainWindow.xib file.

We’re done refactoring, and your Xcode main window should now closely resemble Figure 8-1.

The Twitter Trends application after refactoring

Figure 8-1. The Twitter Trends application after refactoring

Retrieving the trends

Let’s start by writing a class to retrieve the trends using the Twitter API and the NSURLConnection class. Right-click (or Ctrl-click) on the Other Sources group in the Groups & Files pane in Xcode, select Add→New File, and select the Objective-C class, making it a subclass of NSObject. Name the new class “TwitterTrends” when prompted and click Finish.

Note

Except for the contents of the connectionDidFinishLoading: method, this new class is going to be almost identical in structure to the WeatherForecast class we wrote in Chapter 7.

Open the TwitterTrends.h interface file in the Xcode editor. We’re going to need a method to allow us to make the request to the Search Service. We’re going to trigger the request from the RootViewController class. We’ll need a reference back to the view controller so that we can update the view, so we’ll pass that in as an argument. Add the lines shown in bold:

#import <Foundation/Foundation.h>

@class RootController;

@interface TwitterTrends : NSObject {

RootController *viewController;

NSMutableData *responseData;

NSURL *theURL;

}

- (void)queryServiceWithParent:(UIViewController *)controller;

@end

Now open the TwitterTrends.m implementation file in the Xcode editor. If you compare the following code with the code in the WeatherForecast class from Chapter 7, you’ll see that the code is virtually identical:

#import "TwitterTrends.h"

#import "RootController.h"

@implementation TwitterTrends

- (void)queryServiceWithParent:(UIViewController *)controller {

viewController = (RootController *)controller;

responseData = [[NSMutableData data] retain];

NSString *url =

[NSString stringWithFormat:@"http://search.twitter.com/trends.json"];

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

NSURLRequest *request = [NSURLRequest requestWithURL:theURL];

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

}

- (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 {

// Handle Error

}

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

NSString *content = [[NSString alloc] initWithBytes:[responseData bytes]

length:[responseData length]

encoding:NSUTF8StringEncoding];

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

}

-(void)dealloc {

[viewController release];

[responseData release];

[theURL release];

[super dealloc];

}

@end

OK, now that we have a class that can query the Twitter Search Service, let’s use it. Inside the viewDidLoad: method of the RootController.m file add the following two lines of code (you must also uncomment the method by removing the /* before it and the */ after it):

TwitterTrends *trends = [[TwitterTrends alloc] init];

[trends queryServiceWithParent:self];

We also have to import the TwitterTrends.h header file once these have been added, so add the following line to the top of the file:

#import "TwitterTrends.h"

This is a good point to check our code. Make sure you’ve saved your changes and click the Build and Run button in the Xcode toolbar to compile and deploy your application in iPhone Simulator. We started the asynchronous query of the Search service from the viewDidLoad: method, printing the results to the console log when the query completes. So, once the application has started and you see the gray screen of the default view, open the Debugger Console (Run→Console) from the Xcode menu bar. You should see something similar to Figure 8-2. You’ve successfully retrieved the JSON trends file from the Twitter Search Service.

The console log showing the retrieved JSON document

Figure 8-2. The console log showing the retrieved JSON document

Building a UI

Now that we’ve managed to successfully retrieve the trends data, let’s build a UI for the application. Looking at the JSON file, the obvious UI to implement here is a UITableView. The text in each cell will be the trend name, and when the user clicks on the cell we can open the associated Search Service URL using our WebControllerView.

Let’s start by modifying the RootController class; since this is a simple bare-bones application, we’re going to use the view controller class to both control our view and hold our data model. Open the RootController.h interface file in the Xcode editor and add the code shown in bold:

#import <UIKit/UIKit.h>

@interface RootController : UIViewController

<UITableViewDataSource, UITableViewDelegate>

{

UITableView *serviceView;

NSMutableArray *names;

NSMutableArray *urls;

}

@property (nonatomic, retain) IBOutlet UITableView *serviceView;

@property (nonatomic, retain) NSMutableArray *names;

@property (nonatomic, retain) NSMutableArray *urls;

@end

Make sure you’ve saved your changes, and double-click on the RootView.xib file to open it in Interface Builder. You’ll initially be presented with a blank view (if you don’t see it, double-click on the View icon). Drag and drop a navigation bar (UINavigationBar) from the Library window into the View window and position it at the top of the view. Double-click on the title and change it from “Title” to “Twitter Trends”. Now drag and drop a table view (UITableView) into the View window, and resize it to fill the remaining part of the view.

Click on File’s Owner in the main RootView NIB window and change to the Connections Inspector (⌘-2). Click on the serviceView outlet and connect it to your UITableView. Now click on the UITableView and, again in the Connections Inspector, click and connect both thedataSource and delegate outlets to File’s Owner.

That’s it; you’re done in Interface Builder, and you should be looking at something similar to Figure 8-3.

The RootView.xib file in Interface Builder

Figure 8-3. The RootView.xib file in Interface Builder

After making sure you’ve saved your changes to the RootView.xib NIB file, return to Xcode, open the RootController.m implementation file in the Xcode editor, and edit the code so that it looks like this:

#import "RootController.h"

#import "TwitterTrends.h"

@implementation RootController

@synthesize serviceView;

@synthesize names;

@synthesize urls;

- (void)viewDidLoad {

names = [[NSMutableArray alloc] init];1

urls = [[NSMutableArray alloc] init];

[UIApplication

sharedApplication].networkActivityIndicatorVisible = YES;2

TwitterTrends *trends = [[TwitterTrends alloc] init];

[trends queryServiceWithParent:self];3

[super viewDidLoad];

}

- (void)didReceiveMemoryWarning {

[super didReceiveMemoryWarning];

}

- (void)dealloc {

[names dealloc];

[urls dealloc];

[super dealloc];

}

@end

1

Here we initialize the names and urls arrays we declared in the interface file. These will be populated by the TwitterTrends class.

2

This is where we start the network activity indicator in the iPhone status bar spinning. We’ll stop it from the TwitterTrends connectionDidFinishLoading: method.

3

Here is where we start the asynchronous query to the Twitter Search API.

Now we need to implement the UITableViewDelegate methods; we need to implement only three of the delegate methods. Add the following methods to RootController.m:

- (NSInteger)tableView:(UITableView *)tableView

numberOfRowsInSection:(NSInteger)section

{

return names.count;1

}

- (UITableViewCell *)tableView:(UITableView *)tableView

cellForRowAtIndexPath:(NSIndexPath *)indexPath

{

static NSString *CellIdentifier = @"Cell";

UITableViewCell *cell =

[tableView dequeueReusableCellWithIdentifier:CellIdentifier];

if (cell == nil) {

cell = [[[UITableViewCell alloc]

initWithFrame:CGRectZero

reuseIdentifier:CellIdentifier]

autorelease];

}

cell.textLabel.text = [names objectAtIndex:indexPath.row];2

return cell;

}

- (void)tableView:(UITableView *)tableView

didSelectRowAtIndexPath:(NSIndexPath *)indexPath

{

// Add code to handle selection here.

[tableView deselectRowAtIndexPath:indexPath animated:YES];

}

1

We’re going to display a number of cells (equal to names.count) to the user. The names array will be filled from the TwitterTrends instance we created in the viewDidLoad: method.

2

This is where we set the cell label text to be the name of the trending topic.

Click the Build and Run button to test your code. If all goes well, you should still get the JSON document in the Console, but now your view should be a blank table view. Why is it blank? Well, we haven’t parsed the JSON and populated our data model yet. Let’s do that now.

Note

You may also have noticed that the activity indicator keeps spinning. We’ll take care of that, too.

Parsing the JSON document

We need to modify the connectionDidFinishLoading: method to parse the passed JSON document, populate the view controller’s data model, and then request it to reload the table view with the new data.

Parsing JSON is relatively simple, as you will have to work with only one of two structures: either a single object or a list of objects. These map onto an NSDictionary (a key-value pair) or an NSArray, respectively. Replace the implementation of connectionDidFinishLoading: inTwitterTrends.m with the following:

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

NSString *content = [[NSString alloc]

initWithBytes:[responseData bytes]

length:[responseData length]

encoding:NSUTF8StringEncoding];1

SBJSON *parser = [[SBJSON alloc] init];2

NSDictionary *json = [parser objectWithString:content];3

NSArray *trends = [json objectForKey:@"trends"];4

for (NSDictionary *trend in trends) {5

[viewController.names addObject:[trend objectForKey:@"name"]];

[viewController.urls addObject:[trend objectForKey:@"url"]];

}

[parser release];

[UIApplication

sharedApplication].networkActivityIndicatorVisible = NO;6

[viewController.serviceView reloadData];7

}

1

Here we take the returned response data and create a string representation.

2

Here we allocate a parser instance.

3

This is where we use the parser to build an NSDictionary (a hash map) of the JSON document.

4

Here is where we extract an array of trend entries from the dictionary, extracting the object for key “trends”.

5

Here we extract the NSDictionary object at each array index, and grab the trend name and URL and populate the view controller using the accessor methods.

6

This is where we stop the network activity indicator spinning.

7

Here we ask the view controller to reload its view.

At the top of TwitterTrends.m, add the following:

#import "JSON/JSON.h"

If you rebuild your application in Xcode and run it, you should get something similar to Figure 8-4. The table view is now populated with the current trending topics on Twitter.

The Twitter Trends application running in iPhone Simulator

Figure 8-4. The Twitter Trends application running in iPhone Simulator

However, clicking on individual cells doesn’t do anything yet, so we need to modify the tableView:didSelectRowAtIndexPath: method to use our WebViewController class. Replace the tableView:didSelectRowAtIndexPath: method in RootController.m with the following:

- (void)tableView:(UITableView *)tableView

didSelectRowAtIndexPath:(NSIndexPath *)indexPath

{

NSString *title = [names objectAtIndex:indexPath.row];

NSURL *url = [NSURL URLWithString:[urls objectAtIndex:indexPath.row]];

WebViewController *webViewController =

[[WebViewController alloc] initWithURL:url andTitle:title];

[self presentModalViewController:webViewController animated:YES];

[webViewController release];

[tableView deselectRowAtIndexPath:indexPath animated:YES];

}

Now that you’re using the WebViewController class, you need to import it into the view controller, so add the following to the top of RootController.m:

#import "WebViewController.h"

If you rebuild the application again and click on one of the trending topics, the web view should open modally and you should see something similar to Figure 8-5.

Normally, when the JSON parser fails, it will return a nil value. However, we can add error handling when parsing the JSON file relatively simply by passing an NSError object to the parser’s objectWithString:error: method. To do this, locate the connectionDidFinishLoading:method in TwitterTrends.m and find the following code:

NSDictionary *json = [parser objectWithString:content];

NSArray *trends = [json objectForKey:@"trends"];

for (NSDictionary *trend in trends) {

[viewController.names addObject:[trend objectForKey:@"name"]];

[viewController.urls addObject:[trend objectForKey:@"url"]];

}

Replace that code with the following:

NSError *error;

NSDictionary *json = [parser objectWithString:content error:&error];

if ( json == nil ) {

UIAlertView *errorAlert = [[UIAlertView alloc]

initWithTitle:@"Error"

message:[error localizedDescription]

delegate:self cancelButtonTitle:nil otherButtonTitles:@"OK", nil];

[errorAlert show];

[errorAlert autorelease];

} else {

NSArray *trends = [json objectForKey:@"trends"];

for (NSDictionary *trend in trends) {

[viewController.names addObject:[trend objectForKey:@"name"]];

[viewController.urls addObject:[trend objectForKey:@"url"]];

}

}

The Twitter Trends web view

Figure 8-5. The Twitter Trends web view

Note

You can verify that this error handler is working by replacing http://search.twitter.com/trends.json in the queryServiceWithParent: method in TwitterTrends.m with a URL that does not return a JSON-formatted response.

Tidying up

There are a few bits and pieces that I haven’t added to this application but that you really should add if you are going to release it. Most of it has to do with error handling; for instance, you should do a reachability check before trying to retrieve the JSON document. However, this example illustrated that retrieving and parsing JSON documents is a relatively simple task. See Apple’s Reachability Class in Chapter 7 for details on implementing this.

Regular Expressions

Regular expressions, commonly known as regexes, are a pattern-matching standard for text processing, and are a powerful tool when dealing with strings. With regular expressions, an expression serves as a pattern to compare with the text being searched. You can use regular expressions to search for patterns in a string, replace text, and extract substrings from the original string.

Introduction to Regular Expressions

In its simplest form, you can use a regular expression to match a literal string; for example, the regular expression “string” will match the string “this is a string”. Each character in the expression will match itself, unless it is one of the special characters +, ?, ., *, ^, $, (, ), [, {, |, or \. The special meaning of these characters can be escaped by prepending a backslash character, \.

We can also tie our expression to the start of a string (^string) or the end of a string (string$). For the string “this is a string”, ^string will not match the string, while string$ will.

We can also use quantified patterns. Here, * matches zero or more times, ? matches zero or one time, and + matches one or more times. So, the regular expression “23*4” would match “1245”, “12345”, and “123345”, but the expression “23?4” would match “1245” and also “12345”. Finally, the expression “23+4” would match “12345” and “123345” but not “1245”.

Unless told otherwise, regular expressions are always greedy; they will normally match the longest string possible.

While a backslash escapes the meaning of the special characters in an expression, it turns most alphanumeric characters into special characters. Many special characters are available; however, the main ones are:

\d

Matches a numeric character

\D

Matches a nonnumeric character

\s

Matches a whitespace character

\S

Matches a nonwhitespace character

\w

Matches an alphanumeric (or the underscore) character

\W

Matches the inverse of \w

All of these special character expressions can be modified by the quantifier modifiers.

Many other bits of more complicated and advanced syntax are available. If you find yourself making heavy use of regexes, I recommend the books Regular Expressions Cookbook by Jan Goyvaerts and Steven Levithan and Mastering Regular Expressions, Third Edition by Jeffrey E. F. Friedl (both from O’Reilly).

RegexKitLite

Unfortunately, there is no built-in support for regular expressions in Objective-C, or as part of the Cocoa Touch framework. However, the RegexKitLite library adds regular expression support to the base NSString class. See http://regexkit.sourceforge.net/RegexKitLite/.

Warning

RegexKitLite uses the regular expression engine provided by the ICU library. Apple does not officially support linking directly to the libicucore.dylib library. Despite this, many iPhone applications are available on the App Store that use this library, and it is unlikely that Apple will reject your application during the App Store review process for making use of it. However, if you’re worried about using the ICU library, there are alternatives, such as the libregex wrapper GTMRegex provided as part of the Google Toolbox for Mac.

To add RegexKitLite to your own project, download the RegexKitLite-<X.X>.tar.bz2 compressed tarball (X.X will be the current version, such as 3.3), and uncompress and double-click it to extract it. Open the directory and drag and drop the two files, RegexKitLite.h andRegexKitLite.m, into your project. Remember to select the “Copy items into destination group’s folder” checkbox before adding the files.

We’re not done yet; we still need to add the libicucore.dylib library to our project. Double-click on the project icon in the Groups & Files pane in Xcode and go to the Build tab of the Project Info window. In the Linking subsection of the tab, double-click on the Other Linker Flags field and add -licucore to the flags using the pop-up window.

You’ll want to use regular expressions to perform three main tasks: matching strings, replacing strings, and extracting strings. RegexKitLite allows you to do all of these, but remember that when you want to use it, you need to import the RegexKitLite.h file into your class.

Note

Regular expressions use the backslash (\) character to escape characters that have special meaning inside the regular expression. However, since the backslash character is the C escape character, these in turn have to escape any uses of this character inside your regular expression by prepending it with another backslash character. For example, to match a literal ampersand (&) character, you must first prepend it with a backslash to escape it for the regular expression engine, and then prepend it with another backslash to escape this in turn for the compiler—that is, \\&. To match a single literal backslash (\) character with a regular expression therefore requires four backslashes: \\\\.

The RegexKitLite library operates by extending the NSString class via an Objective-C category extension mechanism, making it very easy to use. If you want to match a string, you simply operate directly on the string you want to match. You can create a view-based project and add the following code into the applicationDidFinishLaunching: method. Just be sure to add #import "RegexKitLite.h" to the top of the app delegate’s .m (implementation) file.

NSString *string = @"This is a string";

NSString *match = [string stringByMatching:@"a string$" capture:0];1

NSLog(@"%@", match);

1

This will return the first occurrence of the matched string.

If the match fails, the match variable will be set to nil, and if you want to replace a string, it’s almost as easy:

NSString *string2 = @"This is a string";

NSString *regexString = @"a string$";

NSString *replacementString = @"another string";

NSString *newString = nil;

newString = [string2

stringByReplacingOccurrencesOfRegex:regexString

withString:replacementString];

NSLog(@"%@", newString);

If you run the application, you’ll just get a gray window. Return to Xcode and choose Run→Console to see the output of the NSLog calls.

This will match “a string” in the variable string2, replacing it and creating the string “This is another string” in the variable newString.

While I’ve provided some examples to get you started, it would be impossible to cover regular expressions in detail here, and whole books have been written about this subject. Additionally, the RegexKitLite library provides many other methods on top of those I’ve covered here, so if you need to perform regular expression tasks I haven’t talked about, you might want to look at the documentation, which you can find at http://regexkit.sourceforge.net/RegexKitLite/.

Faking regex support with the built-in NSPredicate

While Cocoa Touch does not provide “real” regular expression support, Core Data does provide the NSPredicate class that allows you to carry out some operations that would normally be done via regular expressions in other languages. For those familiar with SQL, the NSPredicateclass operates in a very similar manner to the SQL WHERE statement.

Let’s assume we have an NSArray of NSDictionary objects, structured like this:

NSArray *arrayOfDictionaries = [NSArray arrayWithObjects:

[NSDictionary dictionaryWithObjectsAndKeys:

@"Learning iPhone Programming", @"title", @"2010", @"year", nil],

[NSDictionary dictionaryWithObjectsAndKeys:

@"Arduino Orbital Lasers", @"title", @"2012", @"year", nil],

nil];

We can test whether a given object in the array matches the criteria foo = "bar" AND baz = "qux" as follows:

NSPredicate *predicate =

[NSPredicate predicateWithFormat:@"year = '2012'"];

for (NSDictionary *dictionary in arrayOfDictionaries) {

BOOL match = [predicate evaluateWithObject:dictionary];

if (match) {

NSLog(@"Found a match!");

}

}

Alternatively, we can extract all entries in the array that match the predicate:

NSPredicate *predicate2 =

[NSPredicate predicateWithFormat:@"year = '2012'"];

NSArray *matches =

[arrayOfDictionaries filteredArrayUsingPredicate:predicate2];

for (NSDictionary *dictionary in matches) {

NSLog(@"%@", [dictionary objectForKey: @"title"]);

}

However, we can also use predicates to test strings against regular expressions. For instance, the following code will test the email string against the regex we provided, returning YES if it is a valid email address:

NSString *email = @"alasdair@babilim.co.uk";

NSString *regex = @"^\\b[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,4}\\b$";

NSPredicate *predicate3 =

[NSPredicate predicateWithFormat:@"SELF MATCHES %@", regex];

BOOL match = [predicate3 evaluateWithObject:email];

if (match) {

NSLog(@"Found a match!");

}

While the NSPredicate class is actually defined as part of the Foundation framework, it is intended (and used extensively) as part of the Core Data framework. We’re not going to cover Core Data in this book. If you’re interested in this framework, I recommend you look at Core Data: Apple’s API for Persisting Data on Mac OS X by Marcus S. Zarra (Pragmatic Programmers).

Storing Data

If the user creates data while running your application, you may need a place to store the data so that it’s there the next time the user runs it. You’ll also want to store user preferences, passwords, and many other forms of data. You could store data online somewhere, but then your application won’t function unless it’s online. The iPhone can store data in lots of ways.

Using Flat Files

So-called flat files are files that contain data, but are typically not backed by the power of a full-featured database system. They are useful for storing small bits of text data, but they lack the performance and organizational advantages that a database provides.

Applications running on the iPhone or iPod touch are sandboxed; you can access only a limited subset of the filesystem from your application. If you want to save files from your application, you should save them into the application’s Document directory.

Here’s the code you need to locate the application’s Document directory:

NSArray *arrayPaths = NSSearchPathForDirectoriesInDomains(

NSDocumentDirectory, NSUserDomainMask, YES);

NSString *docDirectory = [arrayPaths objectAtIndex:0];1

1

The first entry in the array will contain the file path to the application’s Document directory.

Reading and writing text content

The NSFileManager methods generally deal with NSData objects.

For writing to a file, you can use the writeToFile:atomically:encoding:error: method:

NSString *string = @"Hello, World";

NSString *filePath = [docDirectory stringByAppendingString:@"/File.txt"];

[string writeToFile:filePath

atomically:YES

encoding:NSUTF8StringEncoding

error:nil];

If you want to simply read a plain-text file, you can use the NSString class method stringWithContentsOfFile:encoding:error: to read from the file:

NSString *fileContents = [NSString stringWithContentsOfFile:filePath

encoding:NSUTF8StringEncoding error:nil];

NSLog(@"%@", fileContents);

Creating temporary files

To obtain the path to the default location to store temporary files, you can use the NSTemporaryDirectory method:

NSString *tempDir = NSTemporaryDirectory();

Other file manipulation

The NSFileManager class can be used for moving, copying, creating, and deleting files.

Storing Information in an SQL Database

The public domain SQLite library is a lightweight transactional database. The library is included in the iPhone SDK and will probably do most of the heavy lifting you need for your application to store data. The SQLite engine powers several large applications on Mac OS X, including the Apple Mail application, and is extensively used by the latest generation of browsers to support HTML5 database features. Despite the “Lite” name, the library should not be underestimated.

Interestingly, unlike most SQL database engines, the SQLite engine makes use of dynamic typing. Most other SQL databases implement static typing: the column in which a value is stored determines the type of a value. Using SQLite the column type specifies only the type affinity (the recommended type) for the data stored in that column. However, any column may still store data of any type.

Each value stored in an SQLite database has one of the storage types shown in Table 8-1.

Table 8-1. SQLite storage types

Storage type

Description

NULL

The value is a NULL value.

INTEGER

The value is a signed integer.

REAL

The value is a floating-point value.

TEXT

The value is a text string.

BLOB

The value is a blob of data, stored exactly as it was input.

If you’re not familiar with SQL, I recommend you read Learning SQL, Second Edition by Alan Beaulieu (O’Reilly). If you want more information about SQLite specifically, I also recommend SQLite by Chris Newman (Sams).

Adding a database to your project

Let’s create a database for the City Guide application. Open the CityGuide project in Xcode and take a look at the application delegate implementation where we added four starter cities to the application’s data model. Each city has three bits of interesting information associated with it: its name, description, and an associated image. We need to put this information into a database table.

Note

If you don’t want to create the database for the City Guide application yourself, you can download a prebuilt copy containing the starter cities from this book’s website.

Open a Terminal window, and at the command prompt type the code shown in bold:

$ sqlite3 cities.sqlite

This will create a cities database and start SQLite in interactive mode. At the SQL prompt, we need to create our database tables to store our information. Type the code shown in bold (sqlite> and ...> are the SQLite command prompts):

SQLite version 3.4.0

Enter ".help" for instructions

sqlite> CREATE TABLE cities(id INTEGER PRIMARY KEY AUTOINCREMENT,

...> name TEXT, description TEXT, image BLOB);

sqlite> .quit

At this stage, we have an empty database and associated table. We need to add image data to the table as BLOB (binary large object) data; the easiest way to do this is to use Mike Chirico’s eatblob.c program available from http://souptonuts.sourceforge.net/code/eatblob.c.html.

Warning

The eatblob.c code will not compile out of the box on Mac OS X, as it makes use of the getdelim and getline functions. Both of these are GNU-specific and are not made available by the Mac’s stdlib library. However, you can download the necessary source code from http://learningiphoneprogramming.com/.

Once you have downloaded the eatblob.c source file along with the associated getdelim.[h,c] and getline[h,c] source files, you can compile the eatblob program from the command line:

% gcc -o eatblob * -lsqlite3

So, for each of our four original cities defined inside the app delegate, we need to run the eatblob code:

% ./eatblob cities.sqlite ./London.jpg "INSERT INTO cities (id, name,

description, image) VALUES (NULL, 'London', 'London is the capital of the

United Kingdom and England.', ?)"

to populate the database file with our “starter cities.”

Warning

It’s arguable whether including the images inside the database using a BLOB is a good idea, except for small images. It’s a normal practice to include images as a file and include only metadata inside the database itself; for example, the path to the included image. However, if you want to bundle a single file (with starter data) into your application, it’s a good trick.

We’re now going to add the cities database to the City Guide application. However, you might want to make a copy of the City Guide application before modifying it. Navigate to where you saved the project and make a copy of the project folder, and then rename it, perhaps toCityGuideWithDatabase. Then open the new (duplicate) project inside Xcode and use the Project→Rename tool to rename the project itself.

After you’ve done this, open the Finder again and navigate to the directory where you created the cities.sqlite database file. Open the CityGuide project in Xcode, then drag and drop it into the Resources folder of the CityGuide project in Xcode. Remember to check the box to indicate that Xcode should “Copy items into destination group’s folder.”

To use the SQLite library, you’ll need to add it to your project. Double-click on the project icon in the Groups & Files pane in Xcode and go to the Build tab of the Project Info window. In the Linking subsection of the tab, double-click on the Other Linker Flags field and add -lsqlite3 to the flags using the pop-up window.

Data persistence for the City Guide application

We’ve now copied our database into our project, so let’s add some data persistence to the City Guide application.

Since our images are now inside the database, you can delete the images from the Resources group in the Groups & Files pane in Xcode. Remember not to delete the QuestionMark.jpg file because our add city view controller will need that file.

Warning

SQLite runs much slower on the iPhone than it does in iPhone Simulator. Queries that run instantly on the simulator may take several seconds to run on the iPhone. You need to take this into account in your testing.

If you’re just going to be querying the database, you can leave cities.sqlite in place and refer to it via the application bundle’s resource path. However, files in the bundle are read-only. If you intend to modify the contents of the database as we do, your application must copy the database file to the application’s document folder and modify it from there. One advantage to this approach is that the contents of this folder are preserved when the application is updated, and therefore cities that users add to your database are also preserved across application updates.

We’re going to add two methods to the application delegate (CityGuideDelegate.m). The first copies the database we included inside our application bundle to the application’s Document directory, which allows us to write to it. If the file already exists in that location, it won’t overwrite it. If you need to replace the database file for any reason, the easiest way is to delete your application from the simulator and then redeploy it using Xcode. Add the following method to CityGuideDelegate.m:

- (NSString *)copyDatabaseToDocuments {

NSFileManager *fileManager = [NSFileManager defaultManager];

NSArray *paths =

NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,

NSUserDomainMask, YES);

NSString *documentsPath = [paths objectAtIndex:0];

NSString *filePath = [documentsPath

stringByAppendingPathComponent:@"cities.sqlite"];

if ( ![fileManager fileExistsAtPath:filePath] ) {

NSString *bundlePath = [[[NSBundle mainBundle] resourcePath]

stringByAppendingPathComponent:@"cities.sqlite"];

[fileManager copyItemAtPath:bundlePath toPath:filePath error:nil];

}

return filePath;

}

The second method will take the path to the database passed back by the previous method and populate the cities array. Add this method to CityGuideDelegate.m:

-(void) readCitiesFromDatabaseWithPath:(NSString *)filePath {

sqlite3 *database;

if(sqlite3_open([filePath UTF8String], &database) == SQLITE_OK) {

const char *sqlStatement = "select * from cities";

sqlite3_stmt *compiledStatement;

if(sqlite3_prepare_v2(database, sqlStatement,

-1, &compiledStatement, NULL) == SQLITE_OK) {

while(sqlite3_step(compiledStatement) == SQLITE_ROW) {

NSString *cityName =

[NSString stringWithUTF8String:(char *)

sqlite3_column_text(compiledStatement, 1)];

NSString *cityDescription =

[NSString stringWithUTF8String:(char *)

sqlite3_column_text(compiledStatement, 2)];

NSData *cityData = [[NSData alloc]

initWithBytes:sqlite3_column_blob(compiledStatement, 3)

length: sqlite3_column_bytes(compiledStatement, 3)];

UIImage *cityImage = [UIImage imageWithData:cityData];

City *newCity = [[City alloc] init];

newCity.cityName = cityName;

newCity.cityDescription = cityDescription;

newCity.cityPicture = (UIImage *)cityImage;

[self.cities addObject:newCity];

[newCity release];

}

}

sqlite3_finalize(compiledStatement);

}

sqlite3_close(database);

}

You’ll also have to declare the methods in CityGuideDelegate.m’s interface file, so add the following lines to CityGuideDelegate.h just before the @end directive:

-(NSString *)copyDatabaseToDocuments;

-(void) readCitiesFromDatabaseWithPath:(NSString *)filePath;

In addition, you need to import the sqlite3.h header file into the implementation, so add this line to the top of CityGuideDelegate.m:

#include <sqlite3.h>

After we add these routines to the delegate, we must modify the applicationDidFinishLaunching: method, removing our hardcoded cities and instead populating the cities array using our database. Replace the applicationDidFinishLaunching: method inCityGuideDelegate.m with the following:

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

cities = [[NSMutableArray alloc] init];

NSString *filePath = [self copyDatabaseToDocuments];

[self readCitiesFromDatabaseWithPath:filePath];

navController.viewControllers = [NSArray arrayWithObject:viewController];

[window addSubview:navController.view];

[window makeKeyAndVisible];

}

We’ve reached a good point to take a break. Make sure you’ve saved your changes (⌘-Option-S), and click the Build and Run button on the Xcode toolbar. If all goes well, when your application starts it shouldn’t look different from the City Guide application at the end of Chapter 5.

OK, we’ve read in our data in the application delegate. However, we still don’t save newly created cities; we need to insert the new cities into the database when the user adds them from the AddCityController view. Add the following method to the view controller (AddCityController.m):

-(void) addCityToDatabase:(City *)newCity {

NSArray *paths =

NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,

NSUserDomainMask, YES);

NSString *documentsPath = [paths objectAtIndex:0];

NSString *filePath =

[documentsPath stringByAppendingPathComponent:@"cities.sqlite"];

sqlite3 *database;

if(sqlite3_open([filePath UTF8String], &database) == SQLITE_OK) {

const char *sqlStatement =

"insert into cities (name, description, image) VALUES (?, ?, ?)";

sqlite3_stmt *compiledStatement;

if(sqlite3_prepare_v2(database, sqlStatement,

-1, &compiledStatement, NULL) == SQLITE_OK)

{

sqlite3_bind_text(compiledStatement, 1,

[newCity.cityName UTF8String], -1,

SQLITE_TRANSIENT);

sqlite3_bind_text(compiledStatement, 2,

[newCity.cityDescription UTF8String], -1,

SQLITE_TRANSIENT);

NSData *dataForPicture =

UIImagePNGRepresentation(newCity.cityPicture);

sqlite3_bind_blob(compiledStatement, 3,

[dataForPicture bytes],

[dataForPicture length],

SQLITE_TRANSIENT);

}

if(sqlite3_step(compiledStatement) == SQLITE_DONE) {

sqlite3_finalize(compiledStatement);

}

}

sqlite3_close(database);

}

We also need to import the sqlite3.h header file; add this line to the top of AddCityController.m:

#include <sqlite3.h>

Then insert the call into the saveCity: method, directly after the line where you added the newCity to the cities array. The added line is shown in bold:

if ( nameEntry.text.length > 0 ) {

City *newCity = [[City alloc] init];

newCity.cityName = nameEntry.text;

newCity.cityDescription = descriptionEntry.text;

newCity.cityPicture = nil;

[cities addObject:newCity];

[self addCityToDatabase:newCity];

RootController *viewController = delegate.viewController;

[viewController.tableView reloadData];

}

We’re done. Build and deploy the application by clicking the Build and Run button in the Xcode toolbar. When the application opens, tap the Edit button and add a new city. Make sure you tap Save, and leave edit mode.

Then tap the Home button in iPhone Simulator to quit the City Guide application. Tap the application again to restart it, and you should see that your new city is still in the list.

Congratulations, the City Guide application can now save its data.

Refactoring and rethinking

If we were going to add more functionality to the City Guide application, we should probably pause at this point and refactor. There are, of course, other ways we could have built this application, and you’ve probably already noticed that the database (our data model) is now exposed to theAddCityViewController class as well as the CityGuideDelegate class.

First, we’d change things so that the cities array is only accessed through the accessor methods in the application delegate, and then move all of the database routines into the delegate and wrap them inside those accessor methods. This would isolate our data model from our view controller. We could even do away with the cities array and keep the data model “on disk” and access it directly from the SQL database rather than preloading a separate in-memory array.

Although we could do this refactoring now, we won’t do so in this chapter. However, in your own applications, I suggest that you don’t access SQLite directly. Instead, use Core Data (discussed next) or be sure to move your SQLite calls into the delegate to abstract it from the view controller.

Core Data

Sitting above SQLite, and several other possible low-level data representations, is Core Data. The Core Data framework is an abstraction layer above the underlying data representation. Technically, Core Data is an object-graph management and persistence framework. Essentially, this means that Core Data organizes your application’s model layer, keeping track of changes to objects. It allows you to reverse those changes on demand—for instance, if the user performs an undo command—and then allows you to serialize (archive) the application’s data model directly into a persistent store.

Core Data is an ideal framework for building the model part of an MVC-based application, and if used correctly it is an extremely powerful tool. I’m not going to cover Core Data in this book, but if you’re interested in exploring the Core Data framework, I’ve provided some pointers to further reading in Chapter 14.