Archive

twitter

View based tableviews may contain many subviews. For example, a twitter tableview needs to include outlets for an icon image, date, user name, real name, retweeting, favouriting, replying, following and the text of the tweet. In my twitter implementation I had trouble implementing the resizing of the tableview row to show tweets that extended over more than 2 lines. Instead of resizing the view I decided to truncate tweets that were more than 2 lines long and only show first 2 lines of the tweet. Apple in fact provides the necessary code to count the number of lines needed to display an attributed string in a view. This code was easy to adapt to my purpose. The work is done in the tableview delegate method tableView:viewForTableColumn:row. I created a ‘Tweet’ object to hold the relevant information about each tweet (user details, date, text etc). The ‘Tweets’ array holds the downloaded tweets. RBTwitterCellView is the top view in the tableview. It contains all the subviews mentioned above. The relevant parts of tableView:viewForTableColumn:row are given below.


- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row {
RBTwitterCellView *result = [tableView makeViewWithIdentifier:tableColumn.identifier owner:self];
Tweet *tempTweet = [tweets objectAtIndex:row];
.
(allocate values to the various subviews of 'result' and created the attributed tweet text string)
.
[[[result tweetTextView] textStorage] setAttributedString:attributedTextString];

// The following code calculates the number of lines required to display the tweet in the given view within the tableview.
// If the number of lines required is more than 2 I truncate the tweet after the second line (line2index points to the end
// of the second line).
NSLayoutManager *layoutManager = [[result tweetTextView] layoutManager];
NSUInteger numberOfLines, index, line2index, numberOfGlyphs = [layoutManager numberOfGlyphs];
NSRange lineRange;
for (numberOfLines = 0, index = 0; index 2){
[[result tweetField] setString:[[attributedStatusString string]substringToIndex:line2index]];
}

return result;
}

Twitter recently introduced display requirements for applications which use the twitter API. Previously there were only guidelines for developers. The display requirements are available at
https://dev.twitter.com/terms/display-requirements.

The tasks required in developing a twitter app include:

1. Using the new Twitter API v1.1 to access tweets
2. Parsing the tweet dictionary to extract the data that is relevant to the app
3. Detecting has tags, user names and web addresses in the tweet as these have special display requirements.
4. Using attributed strings to create the display.
5. Displaying the results in a way that meets twitter’s display requirements.
6. Detecting mouse clicks and taking the appropriate action (retweeting, replying and favouriting need to be available).
7. Dealing with the need for variable tableview heights due the different tweet lengths.

Designing a display that meets Twitter’s new requirements required me to learn or refresh my knowledge of a number of Xcode skills. It also required me to become reasonably familiar with the twitter API. Here is a possibly incomplete list of things that I met during the development of the twitter app.

1. The responder chain for mouse events. NSViews respond to mouse events but cells don’t which makes views easier to use in this case.
2. RegexKitLite. I have never looked much at regular expressions so I am glad that this tool exists to scan strings for various components such as hashtags, twitter usernames and web addresses.
3. Creating mouse tracking areas and changing the cursor when it is within those areas (such as over hashtags etc.).
4. Using twitter web intents for retweeting, favouriting or replying to tweets.
5. Creating an attribute dictionary and adding attributes to certain parts of a string.
6. Parsing the tweet dictionary. I didn’t find a good reference for the structure for the twitter dictionary so worked it out from scratch. This isn’t very hard once you can work out how to access tweets. Retweets present a special problem as long retweets are someimes truncated in one part of the tweet dictionary. The full tweet is available elsewhere in the dictionary however so this problem can be overcome.
7. View based tableviews. I had not used these previously. The default tableview appears to be cell based.

Twitter’s new v1.1 API imposes limits on the number of requests that users can make. The limits are detailed at https://dev.twitter.com/docs/rate-limiting/1.1/limits. OSX apps need to be aware of these limits and ensure that users cannot make more requests than they are allocated. Twitter states

“We ask that you honor the rate limit. If you or your application abuses the rate limits we will blacklist it. If you are blacklisted you will be unable to get a response from the Twitter API”

Before making a twitter request I check that the rate limit has not been exceeded. I do this using a requestTwitterStatus method as follows:

-(void) requestTwitterStatus{
NSURL *statusURL = [NSURL URLWithString:@"https://api.twitter.com/1.1/application/rate_limit_status.json?resources=help,users,search,statuses"];
statusRequest = [NSMutableURLRequest requestWithURL:statusURL];
[statusRequest addValue:[NSString stringWithFormat:@"Bearer %@",bearerToken] forHTTPHeaderField:@"Authorization"];
[statusRequest setHTTPMethod:@"GET"];
statusConnection = [[NSURLConnection alloc] initWithRequest:statusRequest delegate:self startImmediately:YES];
}

Here the app’s bearer token is the Authorization token obtained from twitter (see my previous post for details of how to obtain this). The connectionDidFinishLoading method detects when the request has finished. The data obtained is then parsed within connectionDidFinishLoading with the following code (receivedData is the data that the getTweetsConnection has provided and is collected in the connection:(NSURLConnection *)connection didReceiveData:(NSData *)data method);

if (connection == statusConnection) {
NSString *twitterStatus = [[NSString alloc] initWithBytes:[statusData bytes] length:[statusData length] encoding:NSUTF8StringEncoding];
NSDictionary *statusResults = [twitterStatus JSONValue];
NSDictionary *statusDict = [statusResults valueForKey:@"resources"];
NSDictionary *searchStatusDict = [statusDict valueForKey:@"search"];
NSDictionary *searchDict = [searchStatusDict valueForKey:@"/search/tweets"];
requestsRemaining = [[searchDict valueForKey:@"remaining"] intValue];
NSLog(@"requestsRemaining = %d\n", requestsRemaining);
if (requestsRemaining > 0) {
// go ahead and make a request to twitter
} else{
NSLog(@"You have reached twitter's request limit. Please wait 15 minutes before requesting again");
}
return;
}

Twitter decommissioned v1 of its API in June 2013. The new version, v1.1, enforces Authentication and Display requirements. Currently my interest is in accessing tweets by certain users or in certain subject areas. Previously this involved the ASIHTTPRequest
methods. I would request twitter data via code such as

NSURL *url = [NSURL URLWithString:feed];
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request setDelegate:self];

where the string ‘feed’ was of the form say,

@”http://search.twitter.com/search.json?q=westernbulldogs%20OR%20from%3Awesternbulldogs”

When the request finished a method then parsed the resulting data as follows:

- (void)requestFinished:(ASIHTTPRequest *)request {
NSString *responseString = [[NSString alloc] initWithBytes:[[request responseData] bytes] length:[[request responseData] length] encoding:NSUTF8StringEncoding];
NSDictionary *results = [responseString JSONValue];
NSArray *entries = [[NSArray alloc] initWithArray:[results objectForKey:@"results"]];
if (entries == nil) {
NSLog(@"Failed to parse %@", request.url);
} else{
for (NSDictionary *entry in entries) {
// Parse the data into various fields
 ...
}
}
}

The process of accessing tweets is now more involved. The instructions at https://dev.twitter.com/docs/auth/application-only-auth
were quite helpful and I will summarise the steps I took.

Each app needs to be given a consumer key and consumer secret. The developer arranges this by creating and logging into her twitter developer account
at dev.twitter.com and creating a new application at https://dev.twitter.com/apps. Once this is done the key and secret can be viewed by selecting the app.

The first step in setting up a twitter request is to use the key and secret to obtain a ‘bearer token’. The key and secret are first encoded according to RFC 1738. The result is then concatenated into one string with a colon as the separator. Finally this string is then base64 encoded. A bearer token request is then made. The following method does this work (I have of course disguised the key and secret):

-(void)tokenOAuth{
NSString *key = @"xxxxxxxxxxxxxxx";
NSString *rfc1738key = [key stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSString *secret = @"yyyyyyyyyyyyyyyyyyyyyyyyyy";
NSString *rfc1738secret = [secret stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSString *concat = [NSString stringWithFormat:@"%@:%@", rfc1738key, rfc1738secret];
NSString *enc = [[concat dataUsingEncoding:NSUTF8StringEncoding] base64Encoding_xcd]; // base64 encoded value of concat
NSURL *theURL = [NSURL URLWithString:@"https://api.twitter.com/oauth2/token"];
getToken = [NSMutableURLRequest requestWithURL:theURL];
[getToken addValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];
NSString *authValue = [NSString stringWithFormat:@"Basic %@", enc];
[getToken addValue:authValue forHTTPHeaderField:@"Authorization"];
NSString *post = @"grant_type=client_credentials";
NSData *body = [post dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES];
[getToken setHTTPMethod:@"POST"];
[getToken setValue:[NSString stringWithFormat:@"%u", (unsigned int)[body length]] forHTTPHeaderField:@"Content-Length"];
[getToken setHTTPBody:body];
getTokenConnection = [[NSURLConnection alloc] initWithRequest:getToken delegate:self startImmediately:YES];
}

The connectionDidFinishLoading: method detects when the connection has finished. This method then parses the data obtained by the connection and checks to see if a bearer token was sent. If so, we can proceed to request the relevant tweets using the bearer token as the authorisation. The connectionDidFinishLoading code is as follows (tokenData is the data that the connection has provided and is collected in the connection:(NSURLConnection *)connection didReceiveData:(NSData *)data method):

- (void)connectionDidFinishLoading:(NSURLConnection *)connection{
if(connection == getTokenConnection){
NSString *tokenString = [[NSMutableString alloc] initWithBytes:[tokenData bytes] length:[tokenData length] encoding:NSUTF8StringEncoding];
if (![tokenData length] == 0) {
results = [tokenString JSONValue];
} else{
results = nil;
printf("Error. No data\n");
return;
}
if([results valueForKey:@"access_token"] && [[results valueForKey:@"token_type"] isEqualToString:@"bearer"]){
bearerToken = [[NSString alloc] initWithString:[results valueForKey:@"access_token"]];
[self requestTweets]; // call a method to request relevant tweets.
}
return;
}

I use the requestTweets method to send a request to twitter for tweets. It looks like this:

- (void) requestTweets{
NSString *feed = @"https://api.twitter.com/1.1/search/tweets.json?q=westernbulldogs%20OR%20from%3Awesternbulldogs" // example request url
NSURL *url = [NSURL URLWithString:feed];
twitterrequest = [NSMutableURLRequest requestWithURL:url];
[twitterrequest addValue:[NSString stringWithFormat:@"Bearer %@",bearerToken] forHTTPHeaderField:@"Authorization"];
[twitterrequest setHTTPMethod:@"GET"];
getTweetsConnection = [[NSURLConnection alloc] initWithRequest:twitterrequest delegate:self startImmediately:YES];
return;
}

Again the connectionDidFinishLoading method detects when the request has finished. The data obtained is then parsed within connectionDidFinishLoading
with the following code (receivedData is the data that the getTweetsConnection has provided and is collected in the connection:(NSURLConnection *)connection didReceiveData:(NSData *)data method);

if(connection == getTweetsConnection){
NSString *responseString = [[NSMutableString alloc] initWithBytes:[receivedData bytes] length:[receivedData length] encoding:NSUTF8StringEncoding];
if (![receivedData length] == 0) {
results = [responseString JSONValue];
} else{
printf("Error:no data obtained\n");
return;
}
// We now need to parse the results
NSArray *entries = [[NSArray alloc] initWithArray:[results objectForKey:@"statuses"]];
if (entries == nil) {
NSLog(@"Failed to parse results");
return;
} else{
for (NSDictionary *entry in entries) {
NSDictionary *userDict = [entry valueForKey:@"user"];
NSString *imageLoc = [userDict valueForKey:@"profile_image_url"];
NSURL *url = [NSURL URLWithString:imageLoc];
NSImage *image = [[NSImage alloc] initWithContentsOfURL:url];
NSString *user = [userDict valueForKey:@"screen_name"];
NSString *name = [userDict valueForKey:@"name"];
NSString *tweetid = [userDict valueForKey:@"id"];
NSString *twitterDateString = [entry valueForKey:@"created_at"];
NSString *dateString = [NSDateFormatter dateStringFromTwitterString:twitterDateString];
NSDate *date = [NSDate dateFromInternetDateTimeString:dateString formatHint:DateFormatHintRFC3339];
NSString *text = [entry valueForKey:@"text"];
Tweet *tweet = [[Tweet alloc] initWithProfileImageLocation:imageLoc icon:image user:user name:name tweetid:tweetid date:date text:text];
[tweets addObject:tweet]; // 'tweets' is the array where the tweets are stored.
}
}

As you can see, you need to do more in v1.1 to get the same information. I have left rate limiting and display requirements out of the above to avoid
complicating matters. In short, Twitter limits the number of requests that users can make and an app needs to ensure that users are not making more requests than they should. A possible penalty for not checking this is blacklisting of the app. In addition Twitter now requires apps to display tweets in certain formats as described on the twitter developer site at https://dev.twitter.com/terms/display-requirements.