Migrating to the new Twitter search API

It is a little over two years ago that I posted about searching Twitter with iOS. In the meantime the Twitter API v1 has been retired and replaced by the Twitter API v1.1. The example code relied on the now defunct Twitter search API to show how to interact with a web service using NSURLConnection. This post will look at what I had to do to migrate to the new API.

A Twitter Search Application

I am not going to repeat all of the detail of the original example TwitterSearch app. The networking code is largely unchanged so refer back to original post for the full details. The basic structure of the App is organised with a RootViewController which prompts the user to enter a search term.

This search query is then passed to a SearchViewController which handles querying the Twitter API and displaying the results in a table view.

Updating to iOS 6

Before looking at the details of the new Twitter API I should mention a number of more general application updates that I have made to the old project. This really shows how much the iOS platform has changed in just two years:

I will not go into the details of these changes as they are not the point of this post but you can take a look at the end result in the GitHub repository for the project.

Twitter Search API v1.1

If you are going to use the new Twitter search API you will probably want to read through the developer documentation at the following links:

To query the old search API you constructed a URL that looked as shown below. This would have returned up to 100 results for the search query “apple” in JSON format.

http://search.twitter.com/search.json?rpp=100&q=apple

The equivalent new API URL is as follows:

https://api.twitter.com/1.1/search/tweets.json?count=100&q=apple

The new API uses https and the session must first be authenticated. There are two possible ways to authenticate with Twitter:

The OAuth signed method allows requests to the Twitter API to be made on behalf of a Twitter user. As we will see this method is directly supported by the iOS SDK making it easy for us to implement.

The application-only authentication mechanism as the name suggests allows requests to be made without a user context. This means you can search without requiring the user to have a Twitter account but it does require that you register your App with Twitter to obtain credentials. As a result I am not going to discuss this method further for now.

The format of the search results has also changed a little. Previously the API returned a JSON dictionary containing an array of “results” where now we get back an array of “statuses”. A number of the fields within the tweet have also changed as the structure of a Tweet has grown.

The Social Framework

The other major change in the last two years is that Apple has added support for social networks in the iOS SDK. This started with the Twitter framework (TWRequest) and Accounts framework (ACAccount) in iOS 5. The ACAccount class makes life easy for us by providing a consistent way of retrieving the Twitter credentials for a user when we need to authenticate the request.

In iOS 6 the Twitter framework was deprecated and replaced with the Social framework (SLRequest) to provide a generalised interface for interacting with social networks including Facebook.

User Authentication

As I mentioned previously we now need to have end user credentials for authentication before making a request to the Twitter search API. The ACAccount framework will take care of this for us by providing an account store organised by account type. At time of writing iOS 6.1 supports accounts for Facebook, Weibo and Twitter. This allows the user to register their Twitter account on an iOS device and authorise applications to access that account.

To get started we need to add a property for an account store to the SearchViewController:

@property (nonatomic,strong) ACAccountStore *accountStore;

I use a lazy initialisation to allocate and initialise the account store:

- (ACAccountStore *)accountStore
{
  if (_accountStore == nil)
  {
    _accountStore = [[ACAccountStore alloc] init];
  }
  return _accountStore;
}

Now when we need to load the query we can check the account store for the available Twitter accounts:

- (void)loadQuery
{
  self.searchState = UYLTwitterSearchStateLoading;
  NSString *encodedQuery = [self.query stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];

  ACAccountType *accountType = [self.accountStore accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierTwitter];
  [self.accountStore requestAccessToAccountsWithType:accountType
                                           options:NULL
                                        completion:^(BOOL granted, NSError *error)
   {
     if (granted)
     {
       ...
     }

Notes:

You should be aware that the user can revoke permissions to your App at any time from the Twitter screen of the Settings application.

Updating the Search State

A small diversion before we get to actually creating the search request. To manage the state of the search request I added a searchState property to the view controller:

@property (nonatomic,assign) UYLTwitterSearchState searchState;

This property is an enumerated type for each of the states that the application can be in when issuing a request. The enumerated type is defined as follows:

typedef NS_ENUM(NSUInteger, UYLTwitterSearchState)
{
  UYLTwitterSearchStateLoading,
  UYLTwitterSearchStateNotFound,
  UYLTwitterSearchStateRefused,
  UYLTwitterSearchStateFailed
};

I will not show it here but the method -searchMessageForState: returns an NSString message for each of these states which is then used to display a message to the user in situations where we do not have search results to display. The relevant code in the tableView:cellForRowAtIndexPath: method is as follows:

NSUInteger count = [self.results count];
if ((count == 0) && (indexPath.row == 0))
{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:LoadCellIdentifier];
    cell.textLabel.text = [self searchMessageForState:self.searchState];
    return cell;
}

So in the situation where the user refuses us permission to the Twitter account type the table view will display as follows:

Creating the Request

Once we have confirmed we have permission to access the Twitter API on behalf of the user we can construct the request. The Social framework hides a lot of the details for us but we still need to construct an SLRequest object with the URL of the search API and the search parameters we want to use:

if (granted)
{
  NSURL *url = [NSURL URLWithString:@"https://api.twitter.com/1.1/search/tweets.json"];
  NSDictionary *parameters = @{@"count" : RESULTS_PERPAGE,
                                   @"q" : encodedQuery};

  SLRequest *slRequest = [SLRequest requestForServiceType:SLServiceTypeTwitter
                                            requestMethod:SLRequestMethodGET
                                                      URL:url
                                               parameters:parameters];

  NSArray *accounts = [self.accountStore accountsWithAccountType:accountType];
  slRequest.account = [accounts lastObject];             

Notes:

Issuing the Request

The SLRequest class provides a block-based method with a completion handler for sending the request (performRequestWithHandler:). However we already have all of the code in place for performing an HTTP request using NSURLConnection that we can continue to use:

NSURLRequest *request = [slRequest preparedURLRequest];
dispatch_async(dispatch_get_main_queue(), ^{
  self.connection = [[NSURLConnection alloc] initWithRequest:request
                                                    delegate:self];
  [UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
});

Notes:

NSURLConnection Delegate

I am not going to go through each of the NSURLConnection delegate methods in detail as for the most part they are unchanged from the original example. For reference we have implemented four delegate methods:

The results of the search query are a JSON stream that we can decode using the NSJSONSerialization framework in -connectionDidFinishLoading:

- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
  [UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
  self.connection = nil;

  NSError *jsonParsingError = nil;
  NSDictionary *jsonResults = [NSJSONSerialization JSONObjectWithData:self.buffer options:0 error:&jsonParsingError];

  self.results = jsonResults[@"statuses"];
  if ([self.results count] == 0)
  {
    NSArray *errors = jsonResults[@"errors"];
    if ([errors count])
    {
        self.searchState = UYLTwitterSearchStateFailed;
    }
    else
    {
        self.searchState = UYLTwitterSearchStateNotFound;
    }
  }

Note that decoding the JSON results should give us a dictionary containing either an array of matching tweets (with key “statuses”) or an array of errors (with key “errors”). Once we have the results we need to force the table view to be reloaded so that we can update the user interface:

  self.buffer = nil;
  [self.tableView reloadData];
  [self.tableView flashScrollIndicators];
}

Updating the Table View

When we receive new results we request the table view to reload its data which will ensure that tableView:cellForRowAtIndexPath: will be called. The relevant code to show the tweet data is as follows:

UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ResultCellIdentifier];    
NSDictionary *tweet = (self.results)[indexPath.row];
cell.textLabel.text = tweet[@"text"];
return cell;

This code makes use of the table view cell prototype defined in the storyboard. Our results data is as an array of dictionaries each representing the data for a single matching tweet. Once we have the dictionary for the current table view row we can then retrieve the tweet data by key. In this case we are just using the tweet “text”. This is most likely a violation of the Twitter display requirements which you should be aware of if you intend to show tweets in a published application.

Pull To Refresh

As a final improvement to the example code I have added support for the pull-to-refresh UIRefreshControl. This mostly consists of just enabling the refresh control for the table view in the storyboard and implementing the corresponding action method when the refresh control is invoked:

The action method in this case repeats the query after first cancelling any existing connection:

- (IBAction)refreshSearchResults
{
  [self cancelConnection];
  [self loadQuery];
}

We also need to dismiss the refresh control when the connection completes or fails by inserting the following method call into connectionDidFinishLoading: and connection:didFailWithError:

[self.refreshControl endRefreshing];

Wrapping Up

It is a shame that Twitter has decided to shut off simple, unauthenticated access to the Twitter API. However if you adopt the Social framework introduced with iOS 6 it is still pretty simple to implement. If you need to support iOS 5 you can follow a similar approach using the TWRequest class. You can find the updated TwitterSearch code in my CodeExamples GitHub repository.

Never miss a post!

iOS Size Classes Cheat Sheet

Subscribe and get my free iOS Size Classes Cheat Sheet

Success! Now check your email to confirm your subscription and download your free guide to iOS Size Classes.

There was an error submitting your subscription. Please try again.

Unsubscribe at any time.
No time to watch WWDC videos?

Sign up to get my iOS posts direct to your inbox and I will send you a free PDF of my iOS Size Classes Cheat Sheet.

OK! Check your inbox (or spam folder) for an email to confirm your details and download your free guide to iOS Size Classes.

There was an error submitting your subscription. Please try again.

Unsubscribe at any time.
Archives Categories