This article will cover building a simple Twitter client that allows users to search for tweets, save those search terms, and recall them at any time. The sample in this article will use Xcode 4 and the iPhone SDK 4.3. All examples are in Objective-C. You can find the code for this article at http://github.com/subdigital/code-mag-twitter-searcher. I encourage you to download the code to help out if you get stuck.
This will be a whirlwind tour, so grab a snack, pull up to the nearest Mac and try it out with me.
A Primer on Objective-C
iOS applications are written in Objective-C, which is a dynamic language based on C. Objective-C takes a little getting used to; so if it is foreign to you, don’t worry. At first it can seem confusing, but eventually the syntax becomes clear. Here is a small primer to get your started:
//calling methods
[someOrder cancel];
/* This is also described as sending the “cancel”
message to the a variable called “someOrder” */
//calling methods with input
[customer setArchived:YES];
[customer setFirstName:@"Darth" lastName:@"Vader"];
/* Notice how the name of the method also helps name
the arguments. Read these examples over again in your
head a few times until you understand what’s going on
here. */
//declare a variable, allocate an instance
SomeObject *someVariable = [[SomeObject alloc] init];
/* Notice here how you can nest square brackets. Here
the “alloc” message is sent to the “SomeObject” class,
allocating some memory for storage. The memory is
junk, however, until it has been initialized. The
return value of “alloc” is sent the “init” message. In
Objective-C, constructors are called initializers and
always start with the word “init”. */
//declare a method
- (void)doSomething {
}
/* The “-“ signifies it as an instance method (a “+”
would indicate static, or class, methods). The return
type is void and the method name is “doSomething” */
//declare a method with input
- (BOOL)isEven:(NSInteger)input {
return input % 2 == 0;
}
/* Here the return type is BOOL (a boolean) and the
method takes an integer argument called “input”. */
For a more detailed tutorial, check out the excellent site, CocoaDevCentral:
http://cocoadevcentral.com/d/learn_objectivec/.
So now that you’re an expert in Objective-C syntax, let’s delve into building an iPhone application. First, we need an IDE.
Getting Xcode 4
Xcode 4 is freely available for Apple Developer Members. If you’re not a member, you can pay $4.99 at the Mac App Store to download Xcode 4. Using Xcode you can build and run applications utilizing the built-in iPhone Simulator. However if you plan to deploy your iPhone apps to a real device you’ll need to cough up $99/year to the Apple iOS Developer Program, located at http://developer.apple.com/ios. Being a member has its benefits though, such as allowing you to run your applications on a device, distribute your applications in the App Store, browsing the forums for help & advice, and getting access to pre-release versions of Xcode and the iOS SDK.
If you have Xcode 3 (version 3.2.6 to be precise) then you can most likely follow along, however, Xcode 4 has changed things significantly.
When you’re ready, launch Xcode 4.
Creating a New iPhone Project in Xcode 4
When you first launch Xcode 4, you’re presented with a launch screen inviting you to create an application. Let’s do that now.
You’ll want to select Create a new Xcode project. There are a few project templates to help you get started. For this article, choose the Navigation-based Application, as shown in Figure 2.
![Figure 1: Xcode 4 Welcome screen.](https://codemag.com/Article/Image/1109041/Figure 01.tiff)
![Figure 2: Starting off with the Navigation-based Application template.](https://codemag.com/Article/Image/1109041/Figure 02.tiff)
Choosing the navigation-based template will generate a bare-bones application that is useful for display lists of data. It includes a UINavigationController (useful for traversing lists of data and progressing through various screens) as well as a view controller that derives from UITableViewController. The UITableViewController is perfect for our needs because we’d like to display the Twitter search results in a list.
Go ahead and select Next and give the application a name. You’ll also need to pick a bundle identifier. Typically this is in the format of com.companyname.appname. Don’t worry too much about choosing the correct value here. You can always change it later.
Xcode 4 will generate a basic project that is ready to run. Figure 4 shows the starter project.
![Figure 3: Giving the application a name and bundle identifier.](https://codemag.com/Article/Image/1109041/Figure 03.tiff)
![Figure 4: The initial project created by Xcode.](https://codemag.com/Article/Image/1109041/Figure 04.tiff)
Adding 3rd-Party Libraries
We’ll need an excellent library for simplifying network programming. Namely, we’ll snag ASIHTTPRequest from github at http://github.com/pokeb/asi-http-request. Clone this project (or just download it) and drag all of the files in the Classes folder into our project (you can safely ignore the files in subfolders). In addition, we’ll need to snag Reachability.h and Reachability.m from the External folder. Xcode will ask you if you want to copy or reference the files. Choose Copy files.
For more information on what ASIHTTPRequest is and why it’s awesome, check out http://allseeing-i.com/ASIHTTPRequest/.
You’ll also need a JSON parser. I like the one located at https://github.com/stig/json-framework. Right-click and create a group called “JSON”. Clone the json-framework project (or download the latest zip) and drag all of the files in the Classes folder into the new JSON group. Make sure copy is selected and click Finish.
Feel free to organize these external files to keep things nice and tidy. When you’re done, it should look like Figure 6.
![Figure 5: When you drag files in from Finder, make sure to choose “Copy items”.](https://codemag.com/Article/Image/1109041/Figure 05.tiff)
![Figure 6: Our organized external classes.](https://codemag.com/Article/Image/1109041/Figure 06.tiff)
Adding the Required Frameworks
Since we’ll be making network calls, there are a few additional frameworks that we’ll link to link in to our project. Click on the MyTwitterSearcher project in the left pane, and then select the MyTwitterSearcher target from the middle section. Under the Build Phases tab there is a section called “Link Binary with Libraries.” In it, you need to add the following frameworks:
-
CFNetwork.framework
-
MobileCoreServices.framework
-
SystemConfiguration.framework
-
libz.dylib
Look at Figure 7 for a visual reference on how to do this. Note that Xcode 4 will place these framework references at the root of your project. For better organization, you can drag them to the Frameworks folder on the left pane.
![Figure 7: Add the required frameworks to the project.](https://codemag.com/Article/Image/1109041/Figure 07.tiff)
Now that we’ve added all of the libraries and frameworks we’re going to need, it’s time to Build and Run to make sure that we haven’t made any mistakes.
Running the Application
The application is all ready to run, so when you are ready, press ?+R to run it. The iPhone Simulator should open and display the bare-bones application shown in Figure 8.
![Figure 8: Running the application for the first time. You can see the navigation bar at the top and the table view displaying a bunch of empty “cells”.](https://codemag.com/Article/Image/1109041/Figure 08.tiff)
Our first screen is empty, but eventually will display the meat of our application: tweets. The UITableView is made up of UITableViewCells, which are the rows you see in Figure 8. In order to “bind” this table view to a data source, you’ll have to supply it with a datasource and a delegate.
The datasource tells the table view how many sections and rows the table has, as well as provides a cell for a given row when asked. The delegate responds to events generated by the table view, such as when the user taps on a given row.
Before we can answer these questions, we’ll need to be able to actually search Twitter. Let’s switch gears for a minute and start building a class to do this for us.
Building a Twitter Searcher Class
In Objective-C there are no namespaces (unfortunately) so in order to avoid collisions in code you intend to re-use it is common to use a two or three letter prefix that signifies your company or organization. You’ve probably already noticed this prefix in action, as “NS” is a common prefix in Apple’s Foundation classes. “NS” stands for NeXTSTEP. For this example, we’ll use the prefix “CD” for “CODE Magazine” (“CM” is already used by the CoreMedia framework).
Let’s create a new class called CDTwitterSearcher by right-clicking the “MyTwitterSearcher” group on the left bar and selecting “New File” as shown in Figure 9. Choose Objective-C Class and click Next.
![Figure 9: Right-click to add a new class to our project.](https://codemag.com/Article/Image/1109041/Figure 09.tiff)
For the superclass, just choose NSObject (which is the default). When prompted for the filename, type CDTwitterSearcher.m and press Enter.
You should be taken directly to the new implementation file, CDTwitterSearcher.m. For now, you want to design the public interface of your class, so find CDTwitterSearcher.h and click on it.
We really only need one method on this class: the ability to search for some text and get back a response with results. Since this is a smart client application and we’ll make a remote call, it is imperative that we do it in an asynchronous fashion. Otherwise the user interface will lock up while the network call is made and our users will hate us. To do this, you want to design a delegate protocol that allows the searcher to notify its callers that search results have arrived. What this means is the initial call to perform a search isn’t going to return anything.
Since this is a smart client application and we’ll make a remote call, it is imperative that we do it in an asynchronous fashion.
Let’s define the method to kick off the search first. In the header file, type the following method signature. You’ll do this before the @end marker.
- (void)searchTwitter:(NSString *)searchTerm;
A quick explanation of this method is probably needed. First off there’s a dash, so it’s clearly an instance method. Next, we see that it doesn’t return anything, since we’ll be notifying the caller back later. Finally, the method takes a single argument of type NSString that we’re calling searchTerm.
We’ll need a way to call our clients back when the search finishes. An appropriate design for this uses the same delegate pattern that the UITableView uses. Figure 10 describes the suggested sequence of events.
![Figure 10: The delegate design to asynchronously call clients back when their search arrives.](https://codemag.com/Article/Image/1109041/Figure 10.tiff)
Defining the CDTwitterSearcherDelegate Protocol
In the CDTwitterSearcher.h header file, we’ll declare our protocol. Make your header look like the code in Listing 1.
Next we have to write the bare-bones implementation of this class. Crack open CDTwitterSearcher.m and make it look like the code snippet below.
#import "CDTwitterSearcher.h"
#import "ASIHTTPRequest.h"
@implementation CDTwitterSearcher
@synthesize delegate;
- (void)searchTwitter:(NSString *)searchTerm {
//do the search
}
@end
To search on Twitter we’ll leverage ASIHTTPRequest to make the network call. Searching on Twitter is very simple. You simply make a HTTP GET request to http://search.twitter.com/search.json?q=my%20search%20term. The result will be a JSON formatted response with all the tweet data we need to build our table.
Make the change to the searchTwitter: method as you see in the next snippet.
NSString *formattedQuery = [self
encodeQuery:searchTerm];
NSString *urlString = [NSString stringWithFormat:
@"<a href="http://search.twitter.com/search.json?q=%@">http://search.twitter.com/search.json?q=%@</a>",
formattedQuery];
ASIHTTPRequest *request = [ASIHTTPRequest
requestWithURL:[NSURL URLWithString:urlString]];
request.delegate = self;
[request startAsynchronous];
You’ll notice we used a new method, encodeQuery: to handle properly formatting the query for use in a URL. That method is defined in this next snippet. This method must be defined above the searchTwitter: method.
- (NSString *)encodeQuery:(NSString *)inputString {
return [inputString
stringByAddingPercentEscapesUsingEncoding:
NSUTF8StringEncoding];
}
Our application still doesn’t build because we set the delegate property of the request to self but our class doesn’t yet implement the ASIHTTPRequestDelegate protocol. By now this pattern should be familiar to you. To conform to the protocol, open up the header and change the class declaration to:
#import "ASIHTTPRequest.h"
@interface CDTwitterSearcher :
NSObject<ASIHTTPRequestDelegate> {
This basically promises the compiler that your class implements at the very least the required methods defined in the protocol. You can examine this protocol by holding down ? and double-clicking on the ASIHTTPRequestDelegate symbol.
As it happens, ASIHTTPRequestDelegate only defines @optional methods. We’re going to implement three methods that we are going to implement as part of this protocol. In the CDTwitterSearcher.m file, add the code shown in Listing 2.
All we’re doing so far is logging what’s happening. Let’s test this out by adding some test code to our app delegate class, MyTwitterSearcherAppDelegate.m.
At the top of the file, add an import to the class we’re testing:
#import "CDTwitterSearcher.h"
Then, in the application:didFinishLaunchingWithOptions: method, place the following code at the top of the method:
CDTwitterSearcher *twitterSearcher =
[[CDTwitterSearcher alloc] init];
[twitterSearcher searchTwitter:@"xcode"];
If you build and run the application again (?+R) you should see a log statement in the debug console showing the status of the request, similar to Figure 11.
![Figure 11: The console output for our test Twitter search for “xcode”.](https://codemag.com/Article/Image/1109041/Figure 11.tiff)
You can see the results in Figure 11 that shows that our searcher is working. Go ahead and delete the sample request code. Next up is to parse and create an array of tweet objects that we can send to its delegate.
Creating a Class to Represent a Tweet
We’re going to create a class that will contain all of the properties of a tweet as well as be responsible for parsing the JSON representation of a tweet.
Create a new class called CDTweet. Make the header file look like this next snippet.
@interface CDTweet : NSObject {
}
@property (nonatomic, copy) NSString *author;
@property (nonatomic, copy) NSString *text;
@property (nonatomic, copy) NSString *profileImageUrl;
+ (CDTweet *)tweetWithDictionary:(NSDictionary *)dict;
@end
The implementation should look like Listing 3.
Great! We now have a class to represent the tweet search results. Head on over to CDTwitterSearcher.m and make the requestFinished: method look like Listing 4.
You’ll also need to import “SBJSON.h” as well for the JSONValue method to be recognized.
We’ve left error handling out for sake of brevity, but it is important to forward these issues to our delegate so they have a chance to respond (retry, throw an error message, or something else). For an additional challenge when you finish this article, try creating a new protocol method for notifying of errors and handle it in the delegate.
Back to the User Interface
Now that the plumbing is in place, we need to focus on the user interface.
We need to declare the user interface components that we need to interact with on the user interface. Open up RootViewController.h and make it look like this next snippet.
#import <UIKit/UIKit.h>
#import "CDTwitterSearcher.h"
@interface RootViewController : UITableViewController
<UISearchBarDelegate, CDTwitterSearcherDelegate> {
CDTwitterSearcher *_twitterSearcher;
NSArray *_tweets;
}
@property (nonatomic, retain) IBOutlet UISearchBar
*searchBar;
@end
To summarize these changes, we …
- Imported the header for our Twitter searcher, so this class could “see” the class and protocol.
- Made our view controller implement both the CDTwitterSearcherDelegate protocol as well as a UISearchBarDelegate protocol. (We’ll see these methods later.)
- Created two instance variables to store the list of tweets and the searcher instance.
- Created an IBOutlet property in order to wire up a search bar from the user interface.
Next, open up the implementation file, RootViewController.m. First we need to synthesize the property we defined to have the getters and setters created for us:
@synthesize searchBar;
We need to clean up the IBOutlet property (and our instance variables) in viewDidUnload and dealloc. Find those methods near the bottom of the class, and make them look like Listing 5.
When the view is loaded, our class gets a viewDidLoad call, so this is the perfect place to initialize some aspects of our controller. Implement the viewDidLoad method as follows:
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"Search Twitter";
_twitterSearcher = [[CDTwitterSearcher alloc]
init];
_twitterSearcher.delegate = self;
}
Next up, we will implement CDTwitterSearcherDelegate protocol method to respond to a search.
- (void)searcherDidCompleteWithResults:
(NSArray *)results {
[_tweets release];
_tweets = [results retain];
[self.tableView reloadData];
}
- Here we release any previous tweets we had (sending a message to nil is a no-op, so we don’t need a nil check). We retain the results that come back and tell the table view to reload its data.
Laying Out the User Interface
Open up the interface builder file (RootViewController.xib) that was created for you with the project. This is where you will lay out the user interface.
The XIB file already contains a UITableView on the design surface. We want to drag a UISearchBar onto this table view. Make sure the right-side panel is open (you can toggle this with ??+0). At the bottom, there’s a section called Objects. Drag a Search Bar from that toolbox onto the Table View at the top. You should see a highlighted area at the top just before you drop it on as shown in Figure 12.
![Figure 12: Drag a Search Bar from the toolbox to the Table View.](https://codemag.com/Article/Image/1109041/Figure 12.tiff)
It should snap in place and place a Search Bar node on the left side Objects hierarchy. Make sure it’s a child of the table view and not a sibling. Click on the Search Bar and check the boxes entitled Shows Cancel Button and Shows Bookmarks Button as shown in Figure 13.
![Figure 13: Setting the desired options on the Search Bar.](https://codemag.com/Article/Image/1109041/Figure 13.tiff)
Next we need to “wire up” the user interface elements to the RootViewController class. To wire up connections, hold down control and click and drag from one element to another. When you let go of the mouse, you’ll see the available connections to make.
Make the following connections before continuing:
Make sure to save the XIB file and head back over to RootViewController.m.
Handling a Search
If you build and run the application now, you should see an empty table with a Search Bar that you can type into (Figure 14).
![Figure 14: Our interface running in the simulator.](https://codemag.com/Article/Image/1109041/Figure 14.tiff)
How do we handle the “search” action and actually send our query? The answer lies in the UISearchBarDelegate protocol that we applied to our header earlier on. This protocol defines various methods that we might want to implement to be notified of various behaviors. We need to implement only a single method, called whenever someone presses the search button. We will then capture the search term, dismiss the keyboard, and pass the query to Twitter. This snippet shows the implementation.
- (void)searchBarSearchButtonClicked:(UISearchBar *)
aSearchBar {
NSString *searchTerm = aSearchBar.text;
//dismiss the keyboard
[aSearchBar resignFirstResponder];
[_twitterSearcher searchTwitter:searchTerm];
}
We now have a working search, but nothing is displayed on the screen yet.
Displaying the Search Results
To configure the table view to display our search results, we need to implement the UITableViewDatasource methods that were stubbed out for us already. Find and implement the methods shown in Listing 6.
Build and run to see what we have so far. As you can see in Figure 15, the tweets aren’t formatted in a way that is easy to read. Ultimately, we want to be able to display the entire tweet, so it will need to span multiple lines. That means, however, that each tweet cell might be a different height. In addition, we want to display the author’s avatar as well. To address this complexity, we’ll create a custom subclass of UITableViewCell for our tweets.
![Figure 15: Our initial table view UI doesn’t display tweets very well.](https://codemag.com/Article/Image/1109041/Figure 15.tiff)
Creating a Custom Tweet Cell Class
Create a new Objective-C class. When Xcode asks you for the superclass, choose UITableViewCell. Name the file CDTweetCell.m. Define your interface as shown in this code snippet.
#import <UIKit/UIKit.h>
#import "CDTweet.h"
@interface CDTweetCell : UITableViewCell {
}
+ (CDTweetCell *)cellForTableView:(UITableView
*)tableView;
+ (CGFloat)heightForTweet:(CDTweet *)tweet;
- (void)updateForTweet:(CDTweet *)tweet;
@end
Notice that we have two class methods:
-
cellForTableView: A factory method for returning a properly dequeued cell. This is mostly for the benefit of cleaning up the tableView:cellForRowAtIndexPath: method in our RootViewController.
-
heightForTweet: Encapsulates the logic for determining the height of a tweet cell.
We’ve also defined a method we can use to quickly update the properties for a cell. Listing 7 shows the first few method implementations.
In this class we’ve defined an initializer for configuring our cell, a factory method for simplifying de-queuing cells for reuse, and a method to calculate the height based on a sample cell. Finally, we set the properties of a tweet with the updateForTweet: method.
Note that we referenced a default avatar image, which you’ll have to drag into your project. I made an icon (and a 2x sized version for Retina displays) that you can retrieve here: http://www.cl.ly/0h0e2i232P23173E3B46. Drag them into the project, and make sure you select Copy Files. Now when you reference the image named “defaultAvatar.png” the iPhone will automatically pull in the file named “defaultAvatar@2x.png” and look better in Retina displays.
Now that we have our cell ready to go, let’s plug it in! Find the tableView:cellForRowAtIndexPath: method in RootViewController.m and modify it according to the following snippet. You’ll also have to import the tweet cell’s header at the top of the file.
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
CDTweetCell *cell = [CDTweetCell
cellForTableView:tableView];
CDTweet *tweet = [_tweets
objectAtIndex:indexPath.row];
[cell updateForTweet:tweet];
return cell;
}
Notice how this code is much cleaner and simpler than the code we had before. Next up we need to dynamically size the cell based on the method we wrote earlier. Add the method shown below just below the method you just modified.
- (CGFloat)tableView:(UITableView *)tableView
heightForRowAtIndexPath:(NSIndexPath *)indexPath {
CDTweet *tweet = [_tweets
objectAtIndex:indexPath.row];
return [CDTweetCell heightForTweet:tweet];
}
Now we’re ready to test it out. Build and run the application and do a search. It should look like Figure 16.
![Figure 16: Our nearly finished tweet display.](https://codemag.com/Article/Image/1109041/Figure 16.tiff)
Next up, we want to support saving searches. Read on!
Saving Searches
In order to save a search, we want to show a button in the top right to trigger the save. We don’t want this shown until a search has been returned, so we’ll add the button when a search returns some rows. Modify the method as shown here.
- (void)searcherDidCompleteWithResults:(NSArray *)
results {
[_tweets release];
_tweets = [results retain];
[self.tableView reloadData];
id saveButton = [[[UIBarButtonItem alloc]
initWithBarButtonSystemItem:
UIBarButtonSystemItemSave
target:self
action:@selector(saveSearch)]
autorelease];
self.navigationItem.rightBarButtonItem =
saveButton;
}
Next we need to implement the save method we’re referring to. We’re going to store the searches in a system dictionary called NSUserDefaults, which can store user preferences and small, simple data structures. Implement the method shown in Listing 8.
To show the existing saved searches, we’ll capture the Bookmark button press action and display a new view controller to show the list. For this we’re first going to have to create a new view controller entitled SavedSearchesViewController. Make this class derive from UITableViewController. This next snippet shows the header and Listing 9 shows the implementation.
@protocol SavedSearchDelegate <NSObject>
- (void)didSelectSearch:(NSString *)searchTerm;
@end
@interface SavedSearchesViewController :
UITableViewController {
}
@property (nonatomic, assign) id<SavedSearchDelegate>
delegate;
@property (nonatomic, retain) NSArray *savedSearches;
@end
This class is pretty straightforward. We had it a list of items (as an array of NSStrings), and it displays them in a table. When we select one, it notifies us. That sounds reasonable, so let’s plug it in.
Open up RootViewController.h and add the import statement and protocol changes as follows:
#import "SavedSearchesViewController.h"
@interface RootViewController : UITableViewController
<UISearchBarDelegate, CDTwitterSearcherDelegate,
SavedSearchDelegate> {
Next, open up RootViewController.m and add the methods shown in Listing 10.
At this point you should be able to save a search and recall that search by tapping on the bookmark button and choosing a row (Figure 17).
![Figure 17: Choosing a saved search.](https://codemag.com/Article/Image/1109041/Figure 17.tiff)
We now have a completed saved search feature. We utilized the same delegate protocol pattern that we’ve seen numerous times already. We also showed how you could display a modal view controller that contains a nice animation for popping in and out.
Now the only thing that is left is to dynamically fetch the avatars for our tweet search results.
Fetching Avatars Seamlessly
So far we have a list of cells that simply need to have their imageView’s image property pointing to a valid image. It’s trivial to fetch an image; however we cannot do this on the main thread. To do this the proper way, we’ll follow these steps:
In addition, we need to implement a few other optimizations in order to avoid unnecessary work:
- Don’t request the same image twice
- Fetch the images sequentially so we don’t saturate the network with 12 image requests at once.
It is also important to try and group together common behavior in another class so our view controller doesn’t get too large. We’ll start by creating a new class, CDTableViewImageCache. This class can inherit from NSObject. Listing 11 shows the header file.
So far we’ve declared a couple of types to hold our images and our current requests. We also have a property to contain the default image, so we can immediately show an image even if the real image hasn’t been fetched yet.
The implementation is fairly straightforward. We’ll start with the synthesized properties and the initializer, shown below.
Here we’ve initialized a dictionary to store images based on a key (we’ll use the indexPath of the cell for the key). We also create a dictionary to store all of the current requests to avoid requesting the same row image multiple times. Finally, we create an NSOperationQueue with a concurrency level of 1. This helps to control the concurrency of our image requests. Doing this will avoid requesting 12 images at the same time and saturating the network connection.
Next we need to implement the methods shown in Listing 12.
The code in Listing 12 is designed to be called multiple times. The first time (and subsequent times until the image has been downloaded) the code ensures that a request is sent for the image and the default image is returned.
The next step is to handle the response from the request. This is done in Listing 13. We’ll also clean up after ourselves in the dealloc method.
That wraps up our reusable image cache. We can use this in any project that loads remote images for a table view.
Plugging CDTableViewImageCache into Our Controller
In order to use this new class, we’ll first have to import the head in our RootViewController.h file:
#import "CDTableViewImageCache.h"
Next we need to add the instance variable (ivar), also in RootViewController.m:
@interface RootViewController : UITableViewController
<UISearchBarDelegate, CDTwitterSearcherDelegate,
SavedSearchDelegate> {
CDTwitterSearcher *_twitterSearcher;
NSArray *_tweets;
CDTableViewImageCache *_imageCache;
}
Next we need to initialize this in viewDidLoad of RootViewController.m:
- (void)viewDidLoad {
[super viewDidLoad];
_imageCache = [[CDTableViewImageCache alloc]
initWithTableView:self.tableView];
_imageCache.defaultImage = [UIImage
imageNamed:@"defaultAvatar.png"];
...
}
We should also clean this class up in viewDidUnload, which is called if our device runs low on memory. Dumping cached images is a perfect way to ease memory pressure.
- (void)viewDidUnload {
[super viewDidUnload];
[_imageCache release];
_imageCache = nil;
...
}
We also set the variable to nil here, which makes it possible to blindly call release in the dealloc method (since calling any method on nil is a no-op).
- (void)dealloc {
...
[_imageCache release];
[super dealloc];
}
Now that we’ve mindfully implemented the lifecycle of the _imageCache instance, all that’s left to do is call it in the tableView:cellForRowAtIndexPath: method.
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
CDTweetCell *cell = [CDTweetCell
cellForTableView:tableView];
CDTweet *tweet = [_tweets
objectAtIndex:indexPath.row];
[cell updateForTweet:tweet];
cell.imageView.image = [_imageCache
imageForIndexPath:indexPath
imageUrl:[NSURL
URLWithString:tweet.profileImageUrl]];
return cell;
}
That’s it! That’s the benefit of creating reusable components like our CDTableViewImageCache class. It required minimal change to introduce it into our controller. Contrast this with dumping all of that image requesting and caching code inside this already large controller. The difference is clear.
Running the Finished Application
At this point you should be ready to run the application. Press **?+**R and perform a search. You should see results quickly, and the avatar image should slowly trickle in. Figure 18 shows the end result.
![Figure 18: Running the finished application.](https://codemag.com/Article/Image/1109041/Figure 18.tiff)
As you can see the avatars are displayed on the left, which makes our application complete!
Where to Go From Here?
Congratulations! You have just created a non-trivial iPhone application the exhibits numerous aspects and techniques used in full-featured applications.
You learned how to leverage Xcode 4 to build iPhone applications starting with a simple template. You also incorporated 3rd-party code to easily send network requests and parse JSON responses. You created user interface components, leveraged the ever-so-common delegate-protocol pattern for asynchronous programming, and persisted lightweight data using NSUserDefaults. It wasn’t your typical Hello World example.
So where do you go from here? Armed with these techniques, you can go create your own applications. Or perhaps just improve on what you have so far. Here are some ideas:
- Adapt it for iPad.
- Make usernames and hashtags tap-able, which perform another search for the term.
- Implement an extra final row in the table to “Load More Rows.”
To learn more about iPhone application development, checkout these resources:
-
Intro to iPhone Development at Tekpub by yours truly http://tekpub.com/iphone
-
Beginning iPhone 3 Development by Dave Mark & Jeff LaMarchehttp://<a href="http://www.amazon.com/Beginning-iPhone-Development-Exploring-SDK/dp/1430224592">www.amazon.com/Beginning-iPhone-Development-Exploring-SDK/dp/1430224592</a>
- iTunes-U Stanford iPhone Programming Coursehttp://itunes.stanford.edu
I really hope you enjoyed this article. Now go make some cool apps!
Listing 1: CDTwitterSearcher.h
#import <Foundation/Foundation.h>
//define our protocol
@protocol CDTwitterSearcherDelegate <NSObject>
@required
- (void)searcherDidCompleteWithResults:(NSArray *)results;
@end
@interface CDTwitterSearcher : NSObject {
}
@property (nonatomic, assign) id<CDTwitterSearcherDelegate>
delegate;
- (void)searchTwitter:(NSString *)searchTerm;
@end
Listing 2: Handling ASIHTTPRequest callbacks
- (void)requestStarted:(ASIHTTPRequest *)request {
NSLog(@"Started %@ request for %@",
request.requestMethod,
request.url);
}
- (void)requestFinished:(ASIHTTPRequest *)request {
NSLog(@"Received HTTP %d from %@",
request.responseStatusCode,
request.url);
NSLog(@"Response body: %@", request.responseString);
//TODO: parse response and notify delegate
}
- (void)requestFailed:(ASIHTTPRequest *)request {
NSLog(@"Request failed for %@. The error was: %@",
request.url,
request.error);
//FUTURE: notify delegate of connection issue
}
Listing 3: CDTweet.m
#import "CDTweet.h"
@implementation CDTweet
@synthesize author, text, profileImageUrl;
+ (CDTweet *)tweetWithDictionary:(NSDictionary *)dict {
CDTweet *tweet = [[CDTweet alloc] init];
tweet.author = [dict objectForKey:@"from_user"];
tweet.text = [dict objectForKey:@"text"];
tweet.profileImageUrl =
[dict objectForKey:@"profile_image_url"];
return [tweet autorelease];
}
- (void)dealloc {
[author release];
[text release];
[profileImageUrl release];
[super dealloc];
}
@end
Listing 4: Parsing the response and sending a list of tweets to the delegate
- (void)requestFinished:(ASIHTTPRequest *)request {
NSLog(@"Received HTTP %d from %@",
request.responseStatusCode,
request.url);
NSLog(@"Response body: %@", request.responseString);
if (request.responseStatusCode == 200) {
//parse the json string
NSString *jsonResponse = [request responseString];
NSDictionary *jsonDictionary = [jsonResponse JSONValue];
//create an array to hold the tweets
NSArray *results = [jsonDictionary objectForKey:@"results"];
NSMutableArray *tweets = [NSMutableArray array];
//build a tweet object and add it to the array
for(id result in results) {
[tweets addObject:[CDTweet tweetWithDictionary:result]];
}
//notify the delegate
[self.delegate searcherDidCompleteWithResults:tweets];
} else {
//FUTURE: Notify of invalid response
}
}
Listing 5: Cleaning up our properties and ivars in RootViewController.m
- (void)viewDidUnload {
[super viewDidUnload];
self.searchBar = nil;
[_twitterSearcher release];
_twitterSearcher = nil;
[_tweets release];
_tweets = nil;
}
- (void)dealloc {
[searchBar release];
[_tweets release];
[_twitterSearcher release];
[super dealloc];
}
Listing 6: Implementing the table view data source methods
#import "CDTweet.h" // (at the top of the file)
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section {
return [_tweets count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell = [tableView
dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc]
initWithStyle:UITableViewCellStyleSubtitle
reuseIdentifier:CellIdentifier] autorelease];
cell.textLabel.font = [UIFont boldSystemFontOfSize:12];
cell.detailTextLabel.font = [UIFont systemFontOfSize:12];
}
CDTweet *tweet = [_tweets objectAtIndex:indexPath.row];
cell.textLabel.text = tweet.author;
cell.detailTextLabel.text = tweet.text;
return cell;
}
Listing 7: Our custom tweet cell implementation (CDTweetCell.m)
+ (UIFont *)textLabelFont {
return [UIFont boldSystemFontOfSize:13];
}
+ (UIFont *)detailTextLabelFont {
return [UIFont systemFontOfSize:13];
}
+ (CDTweetCell *)cellForTableView:(UITableView *)tableView {
static NSString *identifier = @"CDTWeetCell";
CDTweetCell *cell = (CDTweetCell *)[tableView
dequeueReusableCellWithIdentifier:identifier];
if (cell == nil) {
cell = [[[CDTweetCell alloc]
initWithStyle:UITableViewCellStyleSubtitle
reuseIdentifier:identifier] autorelease];
}
return cell;
}
- (id)initWithStyle:(UITableViewCellStyle)style
reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style
reuseIdentifier:reuseIdentifier];
if (self) {
self.contentView.backgroundColor = [UIColor whiteColor];
self.textLabel.font = [CDTweetCell textLabelFont];
self.detailTextLabel.font = [CDTweetCell
detailTextLabelFont];
self.detailTextLabel.textColor = [UIColor darkGrayColor];
self.detailTextLabel.numberOfLines = 4;
self.selectionStyle = UITableViewCellSelectionStyleNone;
}
return self;
}
+ (CGFloat)heightForTweet:(CDTweet *)tweet {
//create a dummy cell
CDTweetCell *sampleCell = [[CDTweetCell alloc]
initWithStyle:UITableViewCellStyleSubtitle
reuseIdentifier:nil];
[sampleCell updateForTweet:tweet];
sampleCell.imageView.image = [UIImage
imageNamed:@"defaultAvatar.png"];
//force a layout so we can get some calculated label frames
[sampleCell layoutSubviews];
//calculate the sizes of the text labels
CGSize fromUserSize = [tweet.author sizeWithFont:
[CDTweetCell textLabelFont]
constrainedToSize:sampleCell.textLabel.frame.size
lineBreakMode:UILineBreakModeTailTruncation];
CGSize textSize = [tweet.text sizeWithFont: [CDTweetCell
detailTextLabelFont]
constrainedToSize:sampleCell.detailTextLabel.frame.size
lineBreakMode:UILineBreakModeWordWrap];
CGFloat minHeight = 59 + 10; //image height + margin
return MAX(fromUserSize.height + textSize.height + 20,
minHeight);
}
- (void)updateForTweet:(CDTweet *)tweet {
self.textLabel.text = tweet.author;
self.detailTextLabel.text = tweet.text;
self.imageView.image = [UIImage
imageNamed:@"defaultAvatar.png"];
}
- (void)layoutSubviews {
[super layoutSubviews];
//force our avatar to be a constant size
self.imageView.frame = CGRectMake(5, 5, 60, 59);
}
Listing 8: Saving a search in NSUserDefaults
- (void)saveSearch {
NSString *searchTerm = self.searchBar.text;
//check for existing saved searches array
NSMutableArray *savedSearches = [[NSUserDefaults
standardUserDefaults] objectForKey:@"saved_searches"];
if (savedSearches == nil) {
savedSearches = [NSMutableArray array];
}
//don't save the same search term twice
if (![savedSearches containsObject:searchTerm]) {
[savedSearches addObject:searchTerm];
[[NSUserDefaults standardUserDefaults]
setObject:savedSearches forKey:@"saved_searches"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
[[[[UIAlertView alloc] initWithTitle:@"Search saved"
message:@"Your search was saved"
delegate:nil
cancelButtonTitle:@"OK"
otherButtonTitles:nil] autorelease] show];
}
Listing 9: SavedSearchesViewController.m
#import "SavedSearchesViewController.h"
@implementation SavedSearchesViewController
@synthesize delegate, savedSearches;
- (void)viewDidLoad {
[super viewDidLoad];
id leftButton = [[[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemCancel
target:self
action:@selector(cancel)] autorelease];
self.navigationItem.leftBarButtonItem = leftButton;
}
- (void)cancel {
[self.parentViewController
dismissModalViewControllerAnimated:YES];
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section {
return self.savedSearches.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell = [tableView
dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc]
initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:CellIdentifier] autorelease];
}
cell.textLabel.text = [self.savedSearches
objectAtIndex:indexPath.row];
return cell;
}
- (void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[self.delegate didSelectSearch:[self.savedSearches
objectAtIndex:indexPath.row]];
[self.parentViewController
dismissModalViewControllerAnimated:YES];
}
- (void)dealloc {
delegate = nil;
[savedSearches release];
[super dealloc];
}
@end
Listing 10: Displaying the SavedSearchesViewController and responding to a choice
- (void)searchBarBookmarkButtonClicked:(UISearchBar *)searchBar {
id savedSearches = [[NSUserDefaults standardUserDefaults]
objectForKey:@"saved_searches"];
SavedSearchesViewController *ssvc =
[[SavedSearchesViewController alloc]
initWithStyle:UITableViewStylePlain];
ssvc.delegate = self;
ssvc.savedSearches = savedSearches;
UINavigationController *navController = [[UINavigationController
alloc] initWithRootViewController:ssvc];
[self presentModalViewController:navController animated:YES];
[ssvc release];
[navController release];
}
- (void)didSelectSearch:(NSString *)searchTerm {
self.searchBar.text = searchTerm;
[_twitterSearcher searchTwitter:searchTerm];
}
Listing 11: CDTableViewImageCache.h
#import <Foundation/Foundation.h>
#import "ASIHTTPRequest.h"
@interface CDTableViewImageCache : NSObject <ASIHTTPRequestDelegate>
{
NSMutableDictionary *_imageCache;
NSMutableDictionary *_currentRequests;
NSOperationQueue *_queue;
}
- (id)initWithTableView:(UITableView *)tableView;
@property (nonatomic, retain) UITableView *tableView;
@property (nonatomic, retain) UIImage *defaultImage;
- (UIImage *)imageForIndexPath:(NSIndexPath *)indexPath
imageUrl:(NSURL *)url;
@end
@synthesize tableView, defaultImage;
- (id)initWithTableView:(UITableView *)tableView_ {
self = [super init];
if (self) {
self.tableView = tableView_;
_imageCache = [[NSMutableDictionary alloc] init];
_currentRequests = [[NSMutableDictionary alloc] init];
_queue = [[NSOperationQueue alloc] init];
[_queue setMaxConcurrentOperationCount:1];
}
return self;
}
Listing 12: Sending the request for the row images
- (void)sendRequestForImage:(NSURL *)imageUrl
atIndexPath:(NSIndexPath *)indexPath {
if ([_currentRequests objectForKey:indexPath]) {
return; //already requestineg
}
//create the request
ASIHTTPRequest *req = [ASIHTTPRequest requestWithURL:imageUrl];
//enable caching
req.cachePolicy = ASIOnlyLoadIfNotCachedCachePolicy;
req.cacheStoragePolicy = ASICachePermanentlyCacheStoragePolicy;
//make sure we're called back when the request finishes
req.delegate = self;
//need to keep track of the index path
// that this request is for
req.userInfo = [NSDictionary dictionaryWithObject:
indexPath forKey:@"indexPath"];
//make sure we don't request for this index path again
[_currentRequests setObject:req forKey:indexPath];
//add it to our queue (the request is started automatically)
[_queue addOperation:req];
}
- (UIImage *)imageForIndexPath:(NSIndexPath *)indexPath
imageUrl:(NSURL *)url {
//bail early if we've already fetched this image
UIImage *existingImage = [_imageCache objectForKey:url];
if (existingImage) {
return existingImage;
}
[self sendRequestForImage:url atIndexPath:indexPath];
return self.defaultImage;
}
Listing 13: Handling the finished request
- (void)requestFinished:(ASIHTTPRequest *)request {
//pull out the index path
NSIndexPath *indexPath = [request.userInfo
objectForKey:@"indexPath"];
[_currentRequests removeObjectForKey:indexPath];
if (request.responseStatusCode == 200) {
//pull out the raw bytes from the response
NSData *imageData = [request responseData];
//create our image
UIImage *image = [UIImage imageWithData:imageData];
//store it in the cache
[_imageCache setObject:image forKey:request.url];
//reload the row in question
[self.tableView reloadRowsAtIndexPaths:[NSArray
arrayWithObject:indexPath]
withRowAnimation:
UITableViewRowAnimationNone];
}
}
- (void)dealloc {
[_queue cancelAllOperations];
[_queue release];
[_currentRequests release];
[_imageCache release];
[tableView release];
[defaultImage release];
[super dealloc];
}