Archive

Apple

I have an NSWindowController that swaps between various NSViews using animations.

@interface MyWindowController : NSWindowController
{
    IBOutlet NSWindow*  window;
    IBOutlet NSView*    view;
	NSViewController*	myCurrentViewController;	// the current view controller
    FirstViewController *fvc;
    SecondlViewController *svc;    
    CATransition *transition;
}
- (IBAction)changeToFirstView:(id)sender;
- (IBAction)changeToSecondView:(id)sender;
@end


@implementation MyWindowController

- (void)awakeFromNib
{
    transition = [CATransition animation];
    [transition setType:kCATransitionPush];
    [transition setSubtype:kCATransitionFromLeft];
    NSDictionary *ani = [NSDictionary dictionaryWithObject:transition
                                                    forKey:@"subviews"];
    [view setAnimations:ani];
    [view setWantsLayer:YES];   //  turns on Core Animation

    fvc = [[FirstViewController alloc] initWithNibName:@“FirstView" bundle:nil];
    [view addSubview:[fvc view]];
    myCurrentViewController = fvc;
}

- (IBAction)changeToFirstView:(id)sender;
{
        fvc = [[FirstViewController alloc] initWithNibName:@“FirstView" bundle:nil];
        [[view animator] replaceSubview:[myCurrentViewController view] with:[fvc view]];
        myCurrentViewController = fvc;
}

- (IBAction)changeToSecondView:(id)sender;
{
        svc = [[SecondViewController alloc] initWithNibName:@“SecondView" bundle:nil];
        [[view animator] replaceSubview:[myCurrentViewController view] with:[svc view]];
        myCurrentViewController = svc;
}

However, when the secondView was swapped in for the first time, parts of the firstView remained visible within secondView. These parts were NSTextViews. After playing around for a while I discovered that switching off the drawBackground option for the NSTextViews in Interface Builder stopped the views leaking through to secondView. It also meant that the boxes were no longer distinguishable from the rest of the background in the view. I think the correct solution is to use setNeedsDisplay to force view to redraw its contents when the subviews are swapped. The issue may be related to the use of CoreAnimation in the swapping of the subviews. So I have added a line

    [view setNeedsDisplay:YES];

within the changeToFirstView and changeToSecondView methods.

Advertisements

Dynamic libraries are explained in https://developer.apple.com/library/mac/documentation/DeveloperTools/Conceptual/DynamicLibraries/000-Introduction/Introduction.html

There are advantages to using dynamic libraries in applications but extra effort is also required in order for the application to be able to locate and use the library when needed.

Firstly, the install names of the libraries need to be changed so that the target system knows where to find them. Secondly, the dynamic libraries need to be copied into the applications package directory. Finally, it seems that with OS X 10.9 Apple now requires third party libraries to be signed by developers separately. Steps 2 and 3 are accomplished via a script which is placed in the build phases section of Xcode. I needed to add a number of dynamic libraries to an application. One of these was libpari-gmp.dylib and I will use this here as an example.

The first step above is accomplished using the install_name_tool to change the dynamic library install name and dependency names so that the target system knows where to find them. I used otool to find the current install name of the file and dependencies:

otool -L libpari-gmp.dylib

This showed that libpari-gmp.dylib has libgmp.10.dylib as a dependency. The following command line instructions will then change the install names to the same location as the application binary (given by “@executable_path”):

install_name_tool -id @executable_path/libpari-gmp.dylib libpari-gmp.dylib

install_name_tool -change “/path/to/dependency/file/libgmp.10.dylib” @executable_path/libgmp.10.dylib libpari-gmp.dylib

The script that accomplishes steps 2 and 3 above looks like this:

Step 2.

###  Copy the library and dependencies from wherever they are located to the appropriate part of the application package (@executable_path above also known as “”$TARGET_BUILD_DIR/$PRODUCT_NAME.app/Contents/MacOS/“).

cp -f “/path/to/library/libpari-gmp.dylib” “$TARGET_BUILD_DIR/$PRODUCT_NAME.app/Contents/MacOS/libpari-gmp.dylib”;

cp -f “/path/to/library/libgmp.10.dylib” “$TARGET_BUILD_DIR/$PRODUCT_NAME.app/Contents/MacOS/libgmp.10.dylib”

Step 3.

#  Sign the dynamic library as now required by Apple (see http://furbo.org/2013/10/17/code-signing-and-mavericks/ for a good explanation of the change).

LOCATION=”$TARGET_BUILD_DIR/$PRODUCT_NAME.app/Contents/MacOS/”

IDENTITY=“Put you code signing identity here”

codesign –verbose –force –sign “$IDENTITY” “$LOCATION/libpari-gmp.dylib”

codesign –verbose –force –sign “$IDENTITY” “$LOCATION/libgmp.10.dylib

In fact step 1 can be performed after step 2 and this is what I did for the dynamic library libiconv.2.dylib. The script for ,ibiconv2.dylib is

cp -f “/opt/local/lib/libiconv.2.dylib” “$TARGET_BUILD_DIR/$PRODUCT_NAME.app/Contents/MacOS/libiconv.2.dylib”

install_name_tool -id @executable_path/libiconv.2.dylib “$TARGET_BUILD_DIR/$PRODUCT_NAME.app/Contents/MacOS/libiconv.2.dylib”;

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’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.