Ad

Our DNA is written in Swift
Jump

Twitter.framework Tutorial

I have this idea for an app that I would totally use myself. You know I started XcodeJobs.com and the @XcodeJobs twitter feed to go with it to have a channel to retweet all the iOS-related job offerings that flow before my very eyes. For the site I’ve been talking to people to create a login for themselves and post their jobs self-servingly. For the twitter feed I’ve been using the Twitter search features with a variety of search terms to find tweets where company owners of the developers themselves are tweeting about job postings.

Now the reality of the matter is that most of all Tweets are from recruiters, agencies, job sites and other kinds of services that a self-respecting iOS developer does not want anything to do with. So right now I’m manually filtering tweets. There are a variety of criteria that I want to be able to combine to end up at the real good retweet-worthy tweets.

For example I would do a search for “hiring iOS” …

Criteria for exclusion might include:

  • Jack Patrick I found to be a recruiter
  • Jobely.com and Jobrep.com are examples for job websites
  • The jobmakesfun is pointing to a Freelancer site with a ridiculous project description.
  • The one for Viki is pointing to a dubious URL that does not seem to correspond with the hiring company.

On the above screenshot the one tweet by Fable Technologies is the only one I deemed worthy of retweeting. It does not contain link bait, is by the guys for themselves, no commercially interested middle man in sight and they are so daring as to even post their e-mail address.

All of these manual steps surely can be automated. I’m envisioning several black lists of twitter accounts that I want to ignore. And the second big category would be to ignore tweets that are leading to job sites (like Bullhorn Reach) or agencies. The user (i.e. myself) could configure multiple elaborate search queries and have the app filter out all the crap. So ideally this would leave us with the true gems of Twitterdom.

What Twitter Engine to Use?

So I sat down in front of my home iMac and started to think to myself: “OMG that will be hell of a job. Maybe I should go look for a good Twitter engine to use first.”

The reason for this train of thought was that nowadays most of the interaction with the Twitter REST API has to be authenticated via OAuth. Which is why there are a bunch of OAuth frameworks in the wild, including one from Google because their APIs are OAuth-heavy as well. OAuth basically works by having a secret token that you have to add to all URL requests that need authentication. Of course one could program this from scratch but you’d certainly spend quite some time on this. Even using a third party OAuth library would not be straightforward because of the time it takes to figure out how to include it in your app.

All of these reasons would make this small project unrealistic for a couple of stolen hours on a Sunday.

Some people recommended MGTwitterEngine, but then it dawned on me that there was something new that Apple had introduced with iOS 5. So I checked the SDK documentation and – indeed! – besides the TWTweetComposeViewController that everybody now knows about there is a second class that is exactly what we need: TWRequest.

Tw…TW…TWRequest?

Hidden behind the simplest of names is a wrapper that you can do any kind of request with. Think of it as a block-based wrapper around NSURLConnection with all the trimmings needed to also do authentication where necessary.

Let’s dive right in, and start by doing a simple search. We added the Twitter.framework to the linked libraries and in the class where we want to call TWRequest we added <Twitter/Twitter.h>. I prefer to put that in the PCH file so that it is available everywhere without needing an extra inclusion of the header.

The search itself is deceptively simple:

NSURL *searchURL = [NSURL URLWithString:@"http://search.twitter.com/search.json"];
NSDictionary *parameters = [NSDictionary dictionaryWithObject:@"hiring ios" forKey:@"q"];
TWRequest *request = [[TWRequest alloc] initWithURL:searchURL parameters:parameters requestMethod:TWRequestMethodGET];
 
[request performRequestWithHandler:^(NSData *responseData, NSHTTPURLResponse *urlResponse, NSError *error) {
    if (responseData)
    {
        NSError *parseError = nil;
        id json = [NSJSONSerialization JSONObjectWithData:responseData options:0 error:&amp;parseError];
 
        if (!json)
        {
            NSLog(@"Parse Error: %@", parseError);
        }
        else
        {
            NSLog(@"%@", json);
        }
    }
    else
    {
        NSLog(@"Request Error: %@", [error localizedDescription]);
    }
}];

To perform a Twitter request you need to have a couple of ingredients, which you can glean from the Twitter API Documentation, for example for the search function.

We needed the URL which is a composite of:

  • the so-called endpoint: http://search.twitter.com
  • the method name: search
  • the response data format: json

The parameters need to be provided in a dictionary which is also straightforward, the keys are the parameter names and the values are the parameter values. Also we don’t need authentication for search, in fact it is not even supported according to the Twitter docs. This is to make it impossible for Twitter to track you based on what you search for… unlike Google.

TWRequests supports the three HTTP verbs GET, POST and DELETE. The documentation tells you which one to use. Note that different API functions might have different end points.

We could have also chosen xml format for the response format, but there is no simple way to transform that into something useful for us. NSXMLParser works event-based and we would have to do some work to get it into a structure. The second ingredient that we need is also new with iOS 5, Apple provides a handy high performance class to serialize and deserialize JSON data.By passing the responds of the TWRequest into NSJSONSerialization we get an NSDictionary containing the parsed JSON. With JSON and the de-serialization method provided we get a dictionary which is way more convenient.

If the deserialization worked you get a non-nil result, looking like this:

{
    "completed_in" = "0.027";
    "max_id" = 204150933297446912;
    "max_id_str" = 204150933297446912;
    "next_page" = "?page=2&amp;max_id=204150933297446912&amp;q=hiring%20ios";
    page = 1;
    query = "hiring+ios";
    "refresh_url" = "?since_id=204150933297446912&amp;q=hiring%20ios";
    results =     (
                {
            "created_at" = "Sun, 20 May 2012 10:05:50 +0000";
            "from_user" = "fun_programming";
            "from_user_id" = 282845320;
            "from_user_id_str" = 282845320;
            "from_user_name" = jobmakesfun;
            geo = "";
            id = 204150933297446912;
            "id_str" = 204150933297446912;
            "iso_language_code" = en;
            metadata =             {
                "result_type" = recent;
            };
            "profile_image_url" = "http://a0.twimg.com/profile_images/1313148959/jobs_normal.gif";
            "profile_image_url_https" = "https://si0.twimg.com/profile_images/1313148959/jobs_normal.gif";
            source = "&lt;a href="http://twitterfeed.com" rel="nofollow"&gt;twitterfeed&lt;/a&gt;";
            text = "#ipad #project iOS Game Side Scrolling Battle by olimoli123: I require someone to make a game like ... http://t.co/LNqWsmLs #dev #hiring";
            "to_user" = "";
            "to_user_id" = 0;
            "to_user_id_str" = 0;
            "to_user_name" = "";
        },
                {
            "created_at" = "Sun, 20 May 2012 08:18:14 +0000";
            "from_user" = XcodeJobs;
            "from_user_id" = 539747758;
            "from_user_id_str" = 539747758;
            "from_user_name" = "Xcode Jobs";
            geo = "";
            id = 204123856812785665;
            "id_str" = 204123856812785665;
            "iso_language_code" = pl;
            metadata =             {
                "result_type" = recent;
            };
            "profile_image_url" = "http://a0.twimg.com/profile_images/1988345754/xcode-twitter_normal.png";
            "profile_image_url_https" = "https://si0.twimg.com/profile_images/1988345754/xcode-twitter_normal.png";
            source = "&lt;a href="http://angel.co" rel="nofollow"&gt;AngelList&lt;/a&gt;";
            text = "RT @CardFlick: CardFlick (@CardFlick) is hiring a iOS Engineer http://t.co/O7Wf0gtw";
            "to_user" = "";
            "to_user_id" = 0;
            "to_user_id_str" = 0;
            "to_user_name" = "";
        },...

We get the tweets in an NSArray below the “results” key, each tweet being represented by an NSDictionary. If we wanted to refresh the contents of this query we could use the value stated in “refresh_url”. Then we would only see tweets that were made after the ones we had already gotten here.

Also results are paginated. To retrieve subsequent pages there is a “next_page” value that would get us those. The default is to return 15 tweets per “page”. We could easily also increase this number by passing it as the optional “rpp” (results per page) parameter.

Later the same day …

I’m skipping over the part where I’m merging the result dictionary into a CoreData database and using an NSFetchedResultsController to display the tweets. That is a different story which I shall treat another day. Let’s just say, I got it to display in a very basic form and pushing a refresh button would redo the same query and merge the results into a database.

After getting all the basics wired up (CoreData Stack, methods to query by ID for user and message, merging function, table view controller, fetched results controller) I ended up with a view like this:

It is immediately apparent from this that we are seeing only tweets that we want to get rid of. 4 of these Twitter accounts would be blacklisted because of the reasons stated above. Also my own tweets are visible as they came from my own account and start with RT, those where the job tweets I had retweeted. OMG the amount of time this tool will save me not having to visually scan through all of this…

But like every TV cook I’ve already prepared this step as the details are of no interest for this tutorial. I added a long press gesture recognizer to each cell and a red “Blacklist” button that allows me to mark the TwitterUser as blacklisted and remove his tweets from the database. This results in a tremendous improvement.

Oh, look! Fable Technologies is at the top after having blacklisted all the clutter. They should pay me a referral fee for all this free advertising!

Now for the hard part of this tutorial. Now that we have leaned up the result, we also want to be able to retweet individual messages and of course somehow know if we have done so already to prevent a double-retweeting. Unfortunately there does not seem to be an easy we to get the retweet info included in the search results.

Dealing with Accounts

In order to do something that requires authentication we need to ask the global account store for a list of Twitter accounts and have the user pick one. First we add the Accounts.framework in our build phases. Then we add <Accounts/Accounts.h> to our PCH file.

The first time we want access to a given account type (as of iOS 5 only Twitter is supported) we need to ask the user’s permission. Subsequently we can query the ACAccountType if permission has already been given.

- (void)account:(UIBarButtonItem *)sender
{
    ACAccountStore *accountStore = [[ACAccountStore alloc] init];
    ACAccountType *accountType = [accountStore accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierTwitter];
 
    if ([accountType accessGranted])
    {
        // have access already
        [self _showListOfTwitterAccountsFromStore:accountStore];
    }
    else
    {
        // need access first
        [accountStore requestAccessToAccountsWithType:accountType withCompletionHandler:^(BOOL granted, NSError *error) {
 
            if (granted)
            {
                [self _showListOfTwitterAccountsFromStore:accountStore];
            }
            else
            {
                UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error" message:@"Cannot link account without permission" delegate:nil cancelButtonTitle:@"Ok" otherButtonTitles:nil];
                [alert show];
            }
        }];
    }
}

There is no shared instance of the ACAccountStore, so we have to create our own with alloc/init. Then we create the ACAccountType for Twitter and check if we might already have access granted. If not then we ask for it passing a completion handler. For sake of simplicity I opted to just show an action sheet for all the configured accounts. This does not deal with the situation of no configured accounts.

- (void)_showListOfTwitterAccountsFromStore:(ACAccountStore *)accountStore
{
    ACAccountType *accountType = [accountStore accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierTwitter];
    NSArray *twitterAccounts = [accountStore accountsWithAccountType:accountType];
 
    UIActionSheet *actions = [[UIActionSheet alloc] initWithTitle:@"Choose Account to Use" delegate:self cancelButtonTitle:@"Cancel" destructiveButtonTitle:nil otherButtonTitles:nil];
    actions.tag = 2;
 
    NSMutableArray *shownAccounts = [NSMutableArray array];
 
    for (ACAccount *oneAccount in twitterAccounts)
    {
        [actions addButtonWithTitle:oneAccount.username];
        [shownAccounts addObject:oneAccount];
    }
 
    _shownAccounts = [shownAccounts copy];
 
    [actions showInView:self.view];
}

Since I don’t have any block-based action sheet handy I need to remember the list of accounts presented in _shownAccounts IVAR and then in the delegate method for the action sheet I can pick the correct one based on the index. The important thing here is that we end up with an instance of a ACAccount.

Now Let’s Sign This…

Let’s quickly check the Retweet API and what we need to perform such a request.

Just like above for the search we need to construct a URL from these parts:

  • the endpoint: http://api.twitter.com/1
  • the method name: statuses/retweet
  • the identifier of the tweet to retweet: some number
  • the response data format: json
  • no parameters necessary

Here I stumbled across something that was not immediately obvious to me. You have to store the ACAccountStore in an instance variable. Otherwise the ACAccount’s ACAccountType will be released by the autorelease pool. At let’s that’s what happened to me, I got a memory exception and an authentication error in alternation. Could it be that there’s a bug in the Accounts framework? Seems to me that the accountType property of ACAccount should be a strong reference, but it isn’t.

But that aside, there’s no hurt making an IVAR for the account store, the function to retweet would then look like this:

- (void)_retweetMessage:(TwitterMessage *)message
{
    NSString *retweetString = [NSString stringWithFormat:@"http://api.twitter.com/1/statuses/retweet/%@.json", message.identifier];
    NSURL *retweetURL = [NSURL URLWithString:retweetString];
    TWRequest *request = [[TWRequest alloc] initWithURL:retweetURL parameters:nil requestMethod:TWRequestMethodPOST];
    request.account = _usedAccount;
 
    [request performRequestWithHandler:^(NSData *responseData, NSHTTPURLResponse *urlResponse, NSError *error) {
        if (responseData)
        {
            NSError *parseError = nil;
            id json = [NSJSONSerialization JSONObjectWithData:responseData options:0 error:&amp;parseError];
 
            if (!json)
            {
                NSLog(@"Parse Error: %@", parseError);
            }
            else
            {
                NSLog(@"%@", json);
            }
        }
        else
        {
            NSLog(@"Request Error: %@", [error localizedDescription]);
        }
    }];
}

There are three differences here versus the search request. 1) this one is a POST, 2) there are no parameters and 3) we set the account property to the ACAccount we have selected earlier. And if this works we indeed see the retweets show up immediately.

If you get an authentication error, then there’s a problem with the ACAccount. If you get some error complaining that this cannot be done with the given tweet, then most likely you have already retweeted it before. And in case of success you get a response with some retweet status.

Conclusion

There are a couple more touches necessary to make this app really useful. For one, we also need to retrieve the timeline of own retweets to be able to mark the tweets we already looked at. Also the retweet response should somehow be used to mark the tweet as retweeted.

And of course the UI could use much more work. We want to asynchronously show the profile pictures, access and edit the blacklist, decode the shortened URLs and make a blacklist based on those and of course be able to specify multiple search query for a given topic. For the UI I’ll probably use my DTCoreText method that lets me render the tweets with interactive hyperlinks.

But the gist of this post is that if you know about the Twitter and the Account frameworks then it is quite easy to build your own specialized Twitter client. We can extend a heart-felt “Thank You!” to the engineers at Apple who built something as useful as these into the operating system. Interacting with Twitter couldn’t be easier!


Categories: Recipes

6 Comments »

  1. Very cool, thanks for the run-through.

    You might find this little library from Twitter handy for parsing the contents of tweets: urls, hashtags, cashtags, mentioned and replied screennames, etc.

    http://github.com/twitter/twitter-text-objc

  2. Great tutorial!

    One question though, how did you populate your table with the text, name, and profile picture of each tweet?

  3. See the video tutorial here

  4. This is awesome! I believe you can grow more Instagram followers with SocialKingMaker.com help! Look at their site – SocialKingMaker.com