Posts tagged Cocoa
If you’re drawing a string in Cocoa, you may notice discrepancies between how the text is rendered in a NSTextView and how it’s rendered when using the NSString (or NSAttributedString) AppKit drawing methods. This is because of they each use a different default NSTypesetterBehavior, which results in slightly different line spacings. When you draw a string using the drawing convenience methods - like [string drawAtPoint:point] - it uses NSTypesetterBehavior_10_2_WithCompatibility1. If you then place the same NSString (or NSAttributedString) into a NSTextView, it will look different because NSTextView uses NSTypesetterLatestBehavior. This isn’t a huge problem unless you’re rendering them right next to/on top of each other. For example, drawing a string into a NSCell in a NSTableView, then dynamically overlaying a NSTextView for selecting/editing. In that case, the text will jump around and it will look horrible.
Fortunately, after much searching, I found there is an easy solution. You need to change the typesetter behavior of the NSTextView’s underlying NSLayoutManager, since I don’t believe there is a way to the change the typesetter behavior of the string drawing methods.
[[textView layoutManager] setTypesetterBehavior:NSTypesetterBehavior_10_2_WithCompatibility];
That’s it and your text will render exactly the same. You should also do this if you’re programmatically calculating the height of a text block before drawing, or you’ll get one height for the calculation, but when you draw the text, it won’t fit.
One of my biggest annoyances with mac software is how it handles opening links. This is usually a disruptive process that requires you to switch from your current task and application to another. Since I use spaces to partition apps (more info), it often results in not only switching apps, but also spaces, which makes it all the more annoying. Now, it makes sense if you click a link, that you want to view that page right away, but usually I’m not opening links to read them now, but to read them soon.
For example, if I’m reading an email that has links, I want to open them as I read it so I don’t forget to look at them, but I don’t want to have to switch applications mid-sentence just to open a url. This is even more prevalent when reading feeds. I use NetNewsWire for my RSS reader, but I don’t use it to actually read the content. I like going to the site. I just use an RSS reader as means to get there more efficiently. So as I go through the links, if there is something interesting, I open the link. When I’ve gone through all the new items, I have everything open in tabs in Firefox. I find it too jarring to open a link, read it, go back to NNW, open a link, read it, etc. NNW handles this with ease by having a “Open links in background” preference. A simple one checkbox option that makes the experience all that more enjoyable and seamless for me.
So, to all developers out there, if your app involves people opening links, please provide this functionality. It’s the attention to detail on these types of user interactions that really make or break an application. And so there’s no excuse, here’s an objective-c method to do it for you:
// Opens a URL in the default browser in background or foreground
- (void)openURL:(NSString *)url inBackground:(BOOL)background
{
if (background)
{
NSArray* urls = [NSArray arrayWithObject:[NSURL URLWithString:url]];
[[NSWorkspace sharedWorkspace] openURLs:urls withAppBundleIdentifier:nil options:NSWorkspaceLaunchWithoutActivation additionalEventParamDescriptor:nil launchIdentifiers:nil];
}
else
{
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:url]];
}
}
12 lines of code and your app just became 10x more useful to me. While we’re on links and usability. If your app does implement this functionality, make sure to provide some indication that the click actually worked or users will keep clicking the link all the while opening the same page a hundred times in their browser. NetNewsWire handles this by providing a subtle animation and changing the color of the text of the links you opened.
For my app, QuickPic, I needed to show the user the IP address of their iPhone so they could type in the URL to the browser. The iPhone SDK provided no simple way to get the IP Address for the wifi connection. There are some undocumented methods that work ([NSHost addresses]), but I didn’t want to risk them pulling that out of there and my app breaking. So I wrote some C code (cobbled together from various sources) that will loop through the network interfaces and retrieve the IP address.
- (NSString *)getIPAddress
{
NSString *address = @"error";
struct ifaddrs *interfaces = NULL;
struct ifaddrs *temp_addr = NULL;
int success = 0;
// retrieve the current interfaces - returns 0 on success
success = getifaddrs(&interfaces);
if (success == 0)
{
// Loop through linked list of interfaces
temp_addr = interfaces;
while(temp_addr != NULL)
{
if(temp_addr->ifa_addr->sa_family == AF_INET)
{
// Check if interface is en0 which is the wifi connection on the iPhone
if([[NSString stringWithUTF8String:temp_addr->ifa_name] isEqualToString:@"en0"])
{
// Get NSString from C String
address = [NSString stringWithUTF8String:inet_ntoa(((struct sockaddr_in *)temp_addr->ifa_addr)->sin_addr)];
}
}
temp_addr = temp_addr->ifa_next;
}
}
// Free memory
freeifaddrs(interfaces);
return address;
}
#include <ifaddrs.h>
#include <arpa/inet.h>
Note: this code will work with the Simulator as well though the interface may not be en0. The iPhone Simulator seems to just use the underlying active Mac OS X network interface. On my macbook pro using wifi, this is en1, but your mileage may vary.
This will also work in Mac OS X since the iPhone OS and Mac OS X both use a lot of the same unix underpinnings.
Using JSON in Cocoa is simple thanks to an excellent open-source JSON Framework by Stig Brautaset. The framework will decode a JSON string into native Objective-C objects, and vice versa . The project includes a packaged Framework, Mac and iPhone SDKs, as well as the source code. The easiest method is to directly embed the source in your app as it’s pretty lightweight, and will work on both Mac and iPhone apps. Here are step-by-step instructions on how to use JSON in your app. (click the images to enlarge)
2.) Open the dmg, and drag the JSON folder into your project in Xcode. Check “Copy items into destination group’s folder (if needed)” when prompted.
3.) Once the source is embedded into your project, you need to import the framework to use it in your code
#import "JSON.h"
4.) Create a SBJSON object to parse JSON into a native Cocoa object. SBJSON will return either a NSDictionary or NSArray depending on the structure of the data. You should know ahead of time the structure of the JSON object your parsing and whether it’s a single JSON object or an array of JSON objects and use the appropriate Cocoa class.
// Create SBJSON object to parse JSON
SBJSON *parser = [[SBJSON alloc] init];
// parse the JSON string into an object - assuming json_string is a NSString of JSON data
NSDictionary *object = [parser objectWithString:json_string error:nil];
Here’s an example showing how to download the public timeline from Twitter as JSON and parse it.
// Create new SBJSON parser object
SBJSON *parser = [[SBJSON alloc] init];
// Prepare URL request to download statuses from Twitter
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://twitter.com/statuses/public_timeline.json"]];
// Perform request and get JSON back as a NSData object
NSData *response = [NSURLConnection sendSynchronousRequest:request returningResponse:nil error:nil];
// Get JSON as a NSString from NSData response
NSString *json_string = [[NSString alloc] initWithData:response encoding:NSUTF8StringEncoding];
// parse the JSON response into an object
// Here we're using NSArray since we're parsing an array of JSON status objects
NSArray *statuses = [parser objectWithString:json_string error:nil];
// Each element in statuses is a single status
// represented as a NSDictionary
for (NSDictionary *status in statuses)
{
// You can retrieve individual values using objectForKey on the status NSDictionary
// This will print the tweet and username to the console
NSLog(@"%@ - %@", [status objectForKey:@"text"], [[status objectForKey:@"user"] objectForKey:@"screen_name"]);
}
Update (9/16/09): I just wrote a post about debugging API requests that might be helpful if you’re having problems using this with another API - Debugging API requests with HTTP Client
Update (8/16/10): I wrote this article about a year and half ago, and everything in it still perfectly valid (except the version of json-framework which is now at 2.3), and a good approach to using JSON in Cocoa. However, I did want to mention that my current preferred method is to use yajl-objc. Yajl-objc is a set is Objective-C bindings for the C-based YAJL JSON library that works on both Mac OS and iOS. The main benefits of using YAJL, is that it’s quite a bit faster since it’s written in C. I won’t reiterate here how to use yajl-objc as the instructions on the github page are pretty clear.
I just came across a problem while programmatically trying to add a label (NSTextField) to a custom view. I wanted the label to have a clear background so the superview would show through, so I added:
[label setBackgroundColor:[NSColor clearColor]];
For some reason, this made the entire background of the label black. Instead I found you can use the following to achieve the desired effect:
[label setDrawsBackground:NO];