iOS

Build it yourself iOS Twitter client Part 2

Learn how to use refactoring and a third-party resource API to retrieve information for your custom Apple iOS application.

In our first simple Twitter app, we explored some of the basics of creating a project. We did include the Twitter integration but for the sole purpose of posting tweets to an account. In this second part we explore the Twitter API more in depth to build a more robust Twitter client. Let's get started!

Create New Project

Create a new project with the following attributes:

  • Tabbed Application
  • ARC & Storyboards
  • iPhone Only
You should be at a screen like Figure A.

Figure A

And if you navigate to your MainStoryboard file you see a more interesting blueprint. (Figure B)

Figure B

Now we will add two new libraries using the same method of going to your Project Navigator's blue Project Icon, selecting Target and Build Phases.  Using the "+" button add the following Libraries:

  • Accounts Framework
  • Twitter Framework

With that out of the way, notice the following files in your project navigator:

  • Png files
  • FirstViewController
  • SecondViewController

The png files are used to decorate the different tabs in the tabbarcontroller. The @2x files are a larger version of the same image for the retina display.

So that's what we have so far. Let's take a look at what we need. If you think of the typical features a Twitter client has: Public timeline, Your Mentions, Your Timeline, Your Profile & General Info. This will require five different screens and XCode produced only two. So let's think about what we should do first. We can rename the two existing viewcontrollers and create three new ones.

Renaming (Refactoring)

Open up FirstViewController.h file (the interface file) and place your cursor over the class name, as shown in Figure C.

Figure C

Right click over the class name and select Refactor, which will present you with this screen (Figure D) to enter the new name.

Figure D

Name it TweetViewController. You will finally be presented with a confirmation screen (Figure E) that tells you where the changes will be made.

Figure E

Repeat the process for SecondViewController and call it ProfileViewController.

Renaming or Refactoring is pretty cool. Our project is only starting and has few files. However, when you want to rename a class in a big project where you have many other classes importing or referencing the class to be renamed, you will see XCode not only rename the class itself, but refactor all classes that reference it so there are no broken links.

Now let's switch over to the MainStoryboard.storyboard file and select the tab for TweetViewController which is still labeled First. Rename it by double clicking the label and call it Post. Finally rename the SecondView tab "Me". We have two of the five viewcontrollers we need.

  • TweetViewController: Used to be FirstViewController
  • ProfileViewController: Used to be SecondViewController

So let's create the other three. By now you know how to create New Files from the File Menu. Let's just take a look at the features the new viewcontrollers will have:

  • FollowMeViewController: UIViewController subclass, iPhone Type, Not iPad and certainly no Xib because we are already using Storyboards.
  • PublicTimelineViewController: UITableViewController
  • MentionsViewController: UITableViewController

So far we have two viewcontroller objects on our storyboard blueprint and their corresponding class files in our Project Navigator. We just created three new class files in our Project Navigator so we have to create the three new objects for them. Let's do this now by dragging 1 UIViewController and 2 UITableViewControllers from the Object Library on the right hand pane onto our storyboard.

Whenever you do this (add a new viewcontroller object to the storyboard) you must tell XCode which class to associate that object with in the Identity Inspector. You will find the Identity Inspector on the right hand pane as well. First select the appropriate view controller in the storyboard, i.e. the UIViewController, and switch to the Identity Inspector and from the dropdown list select FollowMeViewController. (Figure F)

Figure F

Take a close look at the image above. You can see in the Project Navigator that we have already created the new class files. In the Main Window you can see we renamed the two tabs and that we have dragged the new UIViewController. On the right hand pane we have the Identity Inspector selected and we have identified that new UIViewController as a FollowMeViewController object.

Now do the same for the two UITableViewControllers; identify one as the PublicTimelineViewController class and the other as the MentionsViewController class.

Finally we have to connect these three new viewcontrollers to our tabbarcontroller, which is fairly simple. Right-click over the tabbarcontroller in storyboard and a black table appears over it like shown in Figure G.

Figure G

Click and drag from the filled circle on the right of viewcontrollers over to each of the new view controllers on the storyboard so that each new connection is created. Now rename the title of each tab to visually identify each one.

Prepare the AppDelegate

In the Project Navigator select the blue folder and go to Target Settings, Build Phases and open up Link Libraries so you can add the Twitter and Accounts frameworks. Now go ahead and add this code to AppDelgate:

#import <UIKit/UIKit.h>

#import <Accounts/Accounts.h>

@interface AppDelegate : UIResponder <UIApplicationDelegate>

@property (strong, nonatomic) UIWindow *window;

//Adds Twitter support

@property (strong, nonatomic) ACAccountStore *accountStore;

@property (strong, nonatomic) NSMutableDictionary *profileImages;

@property (strong, nonatomic) ACAccount *userAccount; @end

And this will go in your AppDelegate.m

@synthesize window = _window;

@synthesize accountStore = _accountStore;

@synthesize profileImages = _profileImages;

@synthesize userAccount = _userAccount;

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions

{

// Override point for customization after application launch.

//Twitter

self.accountStore = [[ACAccountStore alloc] init];

self.profileImages = [NSMutableDictionary dictionary];

ACAccountType *twitterType = [self.accountStore

accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierTwitter];

[self.accountStore requestAccessToAccountsWithType:twitterType options:nil completion:^(BOOL granted, NSError *error) {

if (granted)

{

NSArray *twitterAccounts =

[self.accountStore accountsWithAccountType:twitterType];

if ([twitterAccounts count])

{

self.userAccount = [twitterAccounts objectAtIndex:0];

[[NSNotificationCenter defaultCenter] postNotification: [NSNotification notificationWithName:@"TwitterAccountAcquiredNotification" object:nil]]; }

else

{

NSLog(@"No Twitter Accounts");

} }

}];

return YES; }

Twitter API Reference Documentation

Now go to Window | Organizer and in the Documentation tab click on the magnifying glass and type "TWRequest". Select the TWRequest Class Reference from the results and in the Overview section click the link https://dev.twitter.com/docs.

This opens up the Twitter Developer Documentation right inside XCode. Look for a section called REST API and click on it. This section displays a nice list of all the different Twitter interactions you can have. Click on the GET statuses/home_timeline link to be taken to that timeline's information.

This page tells you all you need to know on how to fetch this resource. If you want to fetch mentions or favorites or user timelines or anything else, those are different resources and they all have their way of being called.

Figure H

If you look at the right hand side in the Resource Information box of Figure H, you can see everything. Fetching this resource from twitter requires authentication, you can receive a json format response, using HTTP Get method and some other features. Ok now here comes the cool part. Install Twitter for Mac on your Mac. Once you've configured your account on it, go to Twitter | Preferences and click on the Developer tab in order to check the Show Developer Menu option. After you check it, close Preferences and you now have a Develop option in your Twitter menu bar. The only option inside it is Console, so go ahead and open it and you will get a screen like Figure I.

Figure I

On the left hand side you have a list of the resources, much like in the docs. Select Timelines and then Public Timeline. You can see the GET URL you must use and if you click on the URL window and hit ENTER, you get the results from Twitter. Neat!

PublicTimelineViewController (1)

Now let's just incorporate this knowledge into our Twitter Client. Add these imports to your PublicTimelineViewController.m:

#import "PublicTimelineViewController.h"

#import "AppDelegate.h"

#import "TweetCell.h" #import <Twitter/Twitter.h>
NOTE: Our PublicTimelineViewController will actually be our PrivatTimeline, or Newsfeed. This means it will present us with tweets from those we follow. The actual public timeline in Twitter, I'm not sure many people use it, it would be exhausting.

Notice there is an @interface section in our .m which @end and then there is the familiar @implementation. You may be used to only seeing @implementation in the .m. Recent modifications to ObjC have begun to use this format where you declare private variables in the .m file because you may not want other classes to know about them, which is what the .h file is for. So modify your .m in PublicTimelineViewController to look like this:

@interface PublicTimelineViewController ()

@property (strong, nonatomic) NSArray *tweetsArray;

- (void)getFeed;

- (void)updateFeed:(id)feedData;

@end

Now let's implement this and go to your viewDidLoad method which is the class' starting point:

@implementation PublicTimelineViewController

@synthesize tweetsArray = _tweetsArray;

- (void)viewDidLoad

{

[super viewDidLoad];

// Test or register for the

// TwitterAccountAcquiredNotification.

AppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];

if (appDelegate.userAccount)

{

[self getFeed];

}

// Register for the

// TwitterAccountAcquiredNotification after view loads

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(getFeed) name:@"TwitterAccountAcquiredNotification" object:nil]; }

What we are doing here is first, testing if we indeed have a userAccount configured already. If we do, we can call our getFeed method. Otherwise, we register to receive a notification once the account has indeed been created. Now let's handle what happens on viewDidAppear. Add the following methods:

- (BOOL)canBecomeFirstResponder{

return YES;

}

- (void)viewDidAppear:(BOOL)animated {

[super viewDidAppear:animated];

// Become the first responder so we can receive be notified when // a shake occurs.

[self becomeFirstResponder];

}

- (void)viewDidDisappear:(BOOL)animated{

// Resign the first responder when we are not visible anymore.

[self resignFirstResponder];

[super viewDidDisappear:animated];

}

- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event{

[self getFeed]; }

What we are doing here is making our view becomeFirstResponder when we load and resigning when we disappear. And finally we add a method that will allow us to shake the device in order to call our getFeed method.

Now move onto the TableView data source section and set numberOfSectionsInTable to return 1 and numberOfRowsInSection to return this:

return self.tweetsArray.count;

Since we don't want to have the app rotate to landscape, we can restrict that by adding the following method:

(BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation{

return (interfaceOrientation == UIInterfaceOrientationPortrait); }

Now here comes a big method, the getFeed method. Remember what we are doing is simply creating a server request which will be sent to the Twitter server to handle for us and get back the results in json format. If the result is successful, we call the updateFeed method to show us the results. If the result fails, we must show some alert views for the user to see what happened.

Using the Twitter Resources

Here is the getFeed method:

-(void)getFeed{

//1. Get this resource URL from Twitter Develop Console or API Docs

NSURL *feedURL = [NSURL URLWithString:@"http://api.twitter.com/1/statuses/home_timeline.json"];

NSDictionary *parameters = [NSDictionary dictionaryWithObjectsAndKeys:@"15", @"count", nil];

//2. Create the request

TWRequest *twitterFeed = [[TWRequest alloc] initWithURL:feedURL parameters:parameters requestMethod:TWRequestMethodGET];

//3. Get and pass in the account

AppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];

twitterFeed.account = appDelegate.userAccount;

//4. Set activity indicator

UIApplication *sharedApplication = [UIApplication sharedApplication];

sharedApplication.networkActivityIndicatorVisible = YES;

//5. Get the actual feed and json serialize it

[twitterFeed performRequestWithHandler:^(NSData *responseData, NSHTTPURLResponse *urlResponse, NSError *error) {

if (!error) {

NSError *jsonError = nil;

id feedData = [NSJSONSerialization JSONObjectWithData:responseData options:0 error:&jsonError];

if (!jsonError) {

[self updateFeed:feedData];

} else {

// Show an Alert View

UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Error" message:[jsonError localizedDescription] delegate:nil

cancelButtonTitle:@"OK" otherButtonTitles:nil];

[alertView show];

}

} else {

// Show an Alert View

UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Error" message:[error localizedDescription] delegate:nil

cancelButtonTitle:@"OK" otherButtonTitles:nil];

[alertView show];

}

sharedApplication.networkActivityIndicatorVisible = NO; }]; }

The comments are pretty self-explanatory. Notice that we use blocks here to perform the Twitter request. The block allows us to pass in a block of code to execute once the result returns. Also take note that we are calling the account we created and stored in our AppDelegate. This will read the account stored on your device.

We also use a nice little activity indicator to show the user work is being done. Finally we throw in an updateFeed method that shows what is done once the result is received, which is fill our tweetsArray with the result and reload the table view.

In order to display the tweets properly, we need cells. Let's create our own custom cell class called TweetCell. So create a New File, Objective C, subclassing UITableViewCell. Add 3 outlets; two labels and an imageview for the cell. Make sure and create these controls in your cells in storyboard.

To do so you must select the cells from both the PublicTimeline scene and the MentionsViewController.

Figure J

Then from the Identity Inspector change the cell's type to Custom and make its Identifier ContentCell. Finally simply connect all 3 outlets to their respective UIControls in storyboard.

Now let's code the actual class. Add our properties for the 3 outlets we created in your implementation file, which should now look like this:

TweetCell.h

#import <UIKit/UIKit.h>

@interface TweetCell : UITableViewCell

@property (strong, nonatomic) IBOutlet UILabel *tweetLabel;

@property (strong, nonatomic) IBOutlet UIImageView *userImage;

@property (strong, nonatomic) IBOutlet UILabel *usernameLabel;

@end

And finally implement it by synthesizing your properties and you're done. You don't have to do any specific implementation to override any methods. This means our cell will be using all methods inherent to UITableViewCell except that we will have added 3 outlets to make the cell look the way we want and we can set them to what we want.

Your implementation file should look like this:

TweetCell.m

#import "TweetCell.h"

@implementation TweetCell

@synthesize tweetLabel = _tweetLabel;

@synthesize userImage = _userImage;

@synthesize usernameLabel = _usernameLabel;

- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier

{

self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];

if (self) {

// Initialization code

}

return self;

}

- (void)setSelected:(BOOL)selected animated:(BOOL)animated

{

[super setSelected:selected animated:animated];

// Configure the view for the selected state

}

@end

Okay so that pretty much takes care of the tweet data source. However, a Twitter client wouldn't look acceptable without a user's image, right? So let's think about this - we need to download the text data (which loads quickly) before we download the image data (which takes longer). In a normal cellForRowAtIndexPath (cfraip) method, you would get the text data and assign it to the cell's label and be done. But we have an image to think about.

So for now let's just create a normal cfraip method that looks like this:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{

//1. Dequeue cell

static NSString *CellIdentifier = @"ContentCell";

TweetCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];

//2. Get tweet and user of that tweet which is also a dictionary (look at Twitter>Develop>Console

NSDictionary *currentTweet = [self.tweetsArray objectAtIndex:indexPath.row];

NSDictionary *currentUser = [currentTweet objectForKey:@"user"];

//3. Fill im text and image for cell

cell.usernameLabel.text = [currentUser objectForKey:@"name"];

cell.tweetLabel.text = [currentTweet objectForKey:@"text"]; cell.userImage.image = [UIImage imageNamed:@"SomeDefaultImage.png"];

The previous code simply loads the Twitter name, tweet and a default image you can create. Simply create a 50x50 image so that it can be placed as a placeholder image. Later we will work on getting the actual images. Remember to also create a 100x100 image called image@2x.png so it can be used on retina devices.

So you should get the tweets displaying in your PublicTimelineVC. Now let's take a look at changing that default image for the actual Twitter profile image. Add this code right after the cell.userImage.image line and the return cell line:

//4. Call appdelegate

AppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];

//5. Set image for username, but check first

NSString *userName = cell.usernameLabel.text;

if ([appDelegate.profileImages objectForKey:userName]){

//Check if the profileImages dicionarys' objectForKey for that username already exists, if it does, set it

cell.userImage.image = [appDelegate.profileImages objectForKey:userName];

} else {

// If that user's image doesnt exist, then fetch it from the web by getting a queue from the system...

dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

// Asynchronously fetch the image for that user...

dispatch_async(concurrentQueue, ^{

// Store image link in NSURL

NSURL *imageURL = [NSURL URLWithString: [currentUser objectForKey:@"profile_image_url"]];

// Create NSData object for image data

__block NSData *imageData;

// Fetch synchronously

dispatch_sync(concurrentQueue, ^{

imageData = [NSData dataWithContentsOfURL:imageURL];

//Store it back into the appdelegate's dictionary

[appDelegate.profileImages setObject:[UIImage imageWithData:imageData] forKey:userName];

});

//Update table view's cfraip method

dispatch_sync(dispatch_get_main_queue(), ^{

cell.userImage.image = [appDelegate.profileImages objectForKey:userName];

});

}); }

As you can see, we are adding some code to first, call the appDelegate, then check to see if the image for that user already exists in our profileImages dictionary and if it doesn't, fetch it from the Web. It is the web fetching code block we want to examine more closely.

First we get a queue and then asynchronously create the URL object and NSData, yet synchronously fetch the NSData with the URL. The asynchronous call won't block the main thread, but the synchronous call will download one complete image at a time before proceeding to the next. Finally we store it in the profileImages for later use.

You have a pretty decent Twitter Client so far. In the final part, Part III, we move on to the Mentions, which is pretty much the same work but it uses a different Twitter API resource. You'll fly through it, I promise.

If you missed it, you can check out Part 1 of this Twitter app.

About

My name is Marcio Valenzuela and I am from Honduras. I have been coding in iOS for 5 years and I previously worked on web applications in ASP and PHP/mySQL. I have a few apps up in the app store and am currently working on a couple of Cocos2d games...

2 comments
marciokoko
marciokoko

If you followed this post from Day 1, there was a missing method which has been corrected as of Today.

Editor's Picks