Panic on American ApparelCar in Snow, Part 5Car in Snow, Part 4Car in Snow, Part 3

fill the void

You are a user

Recently, I got an email from Skype saying they activated my Skype To Go number. It was early in the morning, and I hadn’t finished my coffee. I already had a vague discomfort with Skype over their switch from SkypeIn/Skype Pro plans to US/Country/World plans. Skimming the email, I quickly jumped to the conclusion that Skype had somehow lost my original number (which I acquired in 2005) and replaced it with a new number. Going to Skype, I clicked on “Skype To Go” and saw it was indeed the new number. I clicked around a bit more to see if I had the wrong section. When I didn’t find any reference to my original number, I sent a support email to Skype about the problem, asking them how to switch it back. Below is a screenshot of the email:

But of course, Skype hadn’t screwed up. I had confused two separate services: Online Number (SkypeIn) where people can call me and Skype To Go where I can call international numbers through my cell phone using Skype’s low rates. Skype support replied to me within five minutes, allaying my fears about losing my original number and explaining the new Skype To Go feature. The problem was, like most consumers, I don’t read.

Let me repeat that: I don’t read. Moreover, I don’t care. My sole goal with Skype is to have a phone number with voicemail. All of my interactions with the service are in terms of that goal, making the other features just noise. I skimmed that email because the subject said “activate” and “number”, and when it mentioned a number that wasn’t my existing one, I panicked. I’m not familiar enough with Skype’s product line to recognize the difference between SkypeIn and Skype To Go, and frankly, I didn’t even read that part. Less than three minutes passed between seeing the email and emailing support.

It’s a marketing quandary. Users have a very limited view of the products they use. They only care about solving the problems they bought the products for, not utilizing all the accompanying features nor hearing about other available products. Developers live and breath their products. They intentionally added every feature to those products and wrote the accompanying marketing material. Developers read and care deeply; users don’t do either. As developers, we need to keep the user perspective in mind.

The best antidote: remember you are a user. Everyone has interactions like my Skype story. Reflect on those, and think about your own products in that context. What if users only read every 10th word? What if they continually hit “OK” on “Confirm Delete” followed by cursing? I do both as a user, because I want to accomplish a goal, not perform a task with a specific product. Your users will thank you.

Matt Gemmell sums up this viewpoint very well in his “World According to Gemmell” segment in The MDN Show Episode 10.


UITabBarController from a XIB

There are many good iPhone tutorials on creating a UITabBarController directly through code (a more complex implementation) or from the main XIB. However, I want to separate out the UITabBarController UI elements into a separate XIB and load them dynamically.

Most of the code is boilerplate. The key is creating the XIB in Interface Builder so that it hooks itself up when loaded. First, the code. The main controller needs to have an outlet for the tab class.

@interface RootViewController : UITableViewController {
	IBOutlet UITabBarController *tabBarController;
}
@property (nonatomic, retain) UITabBarController *tabBarController;

When the tab instance is needed, we can load it:

- (UITabBarController *)tabBarController {
	if (tabBarController == nil) {
		[[NSBundle mainBundle] loadNibNamed:@"TabBar" owner:self options:nil];
	}
	return tabBarController;
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
	[tabBarController release];
	tabBarController = nil;
}

Next, the XIB. We change the “File’s Owner” object to the main controller class and connect the new tabBarController outlet to the UITabBarController in the XIB. When the XIB is loaded, the tab bar controller is added to the main controller.

I’ve posted an example project to Google Code. See the screenshot below.


System Preferences Pane Lock

Many panes in System Preferences require I authenticate myself to change settings. Apple has a consistent UI for this: the lock icon. Next to it is a little snippet of text telling me why it’s useful: “Click the lock to make changes.” I click it and authenticate, and it opens, enabling various buttons and fields in that pane. “Click the lock to prevent further changes.” I click it again, and it locks, disabling those buttons and fields. This UI and underlying authentication process are included in Cocoa: SFAuthorizationView.

This tutorial creates a simple prefpane with a button and the lock UI, building on two earlier posts about root-level operations and debugging prefpanes. Clicking the button creates an empty file at /var/log/test.txt, an action that requires root-level privilege. The button is grayed out if the lock is locked; it enables when I unlock the lock. The source code for this project is available on Google Code.

Xcode

  1. To start, create a new Xcode project for a PreferencePane, listed in “Standard Apple Plug-ins”.
  2. Add the following frameworks to the project: Security and SecurityInterface.
  3. In the header file, import the SecurityInterface framework and add two IBOutlets:
    #import <PreferencePanes/PreferencePanes.h>
    #import <SecurityInterface/SFAuthorizationView.h>
    
    @interface BDAuthorizePrefPanePref : NSPreferencePane  {
    	IBOutlet SFAuthorizationView *authView;
    	IBOutlet NSButton *touchButton;
    }
    
    - (BOOL)isUnlocked;
    - (IBAction)clickTouch:(id)sender;
    
    @end
    
  4. In the source file, setup the security interface and handle its actions:
    #import "BDAuthorizePrefPanePref.h"
    
    @implementation BDAuthorizePrefPanePref
    
    - (void) mainViewDidLoad {
    	// Setup security.
    	AuthorizationItem items = {kAuthorizationRightExecute, 0, NULL, 0};
    	AuthorizationRights rights = {1, &items};
    	[authView setAuthorizationRights:&rights];
    	authView.delegate = self;
    	[authView updateStatus:nil];
    
    	[touchButton setEnabled:[self isUnlocked]];
    }
    
    - (BOOL)isUnlocked {
    	return [authView authorizationState] == SFAuthorizationViewUnlockedState;
    }
    
    - (IBAction)clickTouch:(id)sender {
    	// Collect arguments into an array.
    	NSMutableArray *args = [NSMutableArray array];
    	[args addObject:@"-c"];
    	[args addObject:@" touch /var/log/test.txt"];
    
    	// Convert array into void-* array.
    	const char **argv = (const char **)malloc(sizeof(char *) * [args count] + 1);
    	int argvIndex = 0;
    	for (NSString *string in args) {
    		argv[argvIndex] = [string UTF8String];
    		argvIndex++;
    	}
    	argv[argvIndex] = nil;
    
    	OSErr processError = AuthorizationExecuteWithPrivileges([[authView authorization] authorizationRef], [@"/bin/sh" UTF8String],
    															kAuthorizationFlagDefaults, (char *const *)argv, nil);
    	free(argv);
    
    	if (processError != errAuthorizationSuccess)
    		NSLog(@"Error: %d", processError);
    }
    
    //
    // SFAuthorization delegates
    //
    
    - (void)authorizationViewDidAuthorize:(SFAuthorizationView *)view {
    	[touchButton setEnabled:[self isUnlocked]];
    }
    
    - (void)authorizationViewDidDeauthorize:(SFAuthorizationView *)view {
    	[touchButton setEnabled:[self isUnlocked]];
    }
    
    @end
    

Interface Builder

  1. Open the xib file in the Resources folder of the new project.
  2. Change the NSFileOwner’s class to the main controller’s class.
  3. Resize the window to W:440,H:120.
  4. Drop an NSView into the content view of the window.
  5. Resize the view to X:20,Y:20,W:400,:H:40.
  6. Change the view’s class to SFAuthorizationView.
  7. Place an NSButton above the security view and link its action to clickTouch:.
  8. Link up the outlets for the button and the security view.

iPhone Tip: No NSHost

On the iPhone, I miss NSHost. CocoaTouch does contain a private version of this Cocoa class, but again, it is private. Recently, I needed to resolve a DNS name to an IP address in an iPhone app. It’s a one-liner in Cocoa: [[NSHost hostWithName:@"www.google.com"] address]. Not so on the iPhone without NSHost. So, with a little help from Apple’s CFHostSample sample project and their docs on CFHost, I put together this short snippet.

2010-02-09 Update:
What about the local IP addresses and ethernet MAC addresses? Building off the code from Zach Waugh (developer of QuickPic), I added two methods for getting the set of IP addresses and ethernet addresses. The wireless connection seems to be en0, while the cellular connection is pdp_ip0. Keep in mind that the IP addresses are what the device believes, not what the outside world sees (What Is My IP?), thanks to NATs.

// MIT license
// Remember to add CFNetwork.framework to your project using Add=>Existing Frameworks.

#import "BDHost.h"
#import <CFNetwork/CFNetwork.h>
#import <netinet/in.h>
#import <netdb.h>
#import <ifaddrs.h>
#import <arpa/inet.h>
#import <net/ethernet.h>
#import <net/if_dl.h>

@implementation BDHost

+ (NSArray *)addressForHostname:(NSString *)hostname {
	NSArray *addresses = [BDHost addressesForHostname:hostname];
	if ([addresses count] > 0)
		return [addresses objectAtIndex:0];
	else
		return nil;
}

+ (NSArray *)addressesForHostname:(NSString *)hostname {
	// Get the addresses for the given hostname.
	CFHostRef hostRef = CFHostCreateWithName(kCFAllocatorDefault, (CFStringRef)hostname);
	BOOL isSuccess = CFHostStartInfoResolution(hostRef, kCFHostAddresses, nil);
	if (!isSuccess) return nil;
	CFArrayRef addressesRef = CFHostGetAddressing(hostRef, nil);
	if (addressesRef == nil) return nil;

	// Convert these addresses into strings.
	char ipAddress[INET6_ADDRSTRLEN];
	NSMutableArray *addresses = [NSMutableArray array];
	CFIndex numAddresses = CFArrayGetCount(addressesRef);
	for (CFIndex currentIndex = 0; currentIndex < numAddresses; currentIndex++) {
		struct sockaddr *address = (struct sockaddr *)CFDataGetBytePtr(CFArrayGetValueAtIndex(addressesRef, currentIndex));
		if (address == nil) return nil;
		getnameinfo(address, address->sa_len, ipAddress, INET6_ADDRSTRLEN, nil, 0, NI_NUMERICHOST);
		if (ipAddress == nil) return nil;
		[addresses addObject:[NSString stringWithCString:ipAddress]];
	}

	return addresses;
}

+ (NSArray *)ipAddresses {
	NSMutableArray *addresses = [NSMutableArray array];
	struct ifaddrs *interfaces = NULL;
	struct ifaddrs *currentAddress = NULL;

	int success = getifaddrs(&interfaces);
	if (success == 0) {
		currentAddress = interfaces;
		while(currentAddress != NULL) {
			if(currentAddress->ifa_addr->sa_family == AF_INET) {
				NSString *address = [NSString stringWithUTF8String:inet_ntoa(((struct sockaddr_in *)currentAddress->ifa_addr)->sin_addr)];
				if (![address isEqual:@"127.0.0.1"]) {
					NSLog(@"%@ ip: %@", [NSString stringWithUTF8String:currentAddress->ifa_name], address);
					[addresses addObject:address];
				}
			}
			currentAddress = currentAddress->ifa_next;
		}
	}
	freeifaddrs(interfaces);
	return addresses;
}

+ (NSArray *)ethernetAddresses {
	NSMutableArray *addresses = [NSMutableArray array];
	struct ifaddrs *interfaces = NULL;
	struct ifaddrs *currentAddress = NULL;
	int success = getifaddrs(&interfaces);
	if (success == 0) {
		currentAddress = interfaces;
		while(currentAddress != NULL) {
			if(currentAddress->ifa_addr->sa_family == AF_LINK) {
				NSString *address = [NSString stringWithUTF8String:ether_ntoa((const struct ether_addr *)LLADDR((struct sockaddr_dl *)currentAddress->ifa_addr))];

				// ether_ntoa doesn't format the ethernet address with padding.
				char paddedAddress[80];
				int a,b,c,d,e,f;
				sscanf([address UTF8String], "%x:%x:%x:%x:%x:%x", &a, &b, &c, &d, &e, &f);
				sprintf(paddedAddress, "%02X:%02X:%02X:%02X:%02X:%02X",a,b,c,d,e,f);
				address = [NSString stringWithUTF8String:paddedAddress];

				if (![address isEqual:@"00:00:00:00:00:00"] && ![address isEqual:@"00:00:00:00:00:FF"]) {
					NSLog(@"%@ mac: %@", [NSString stringWithUTF8String:currentAddress->ifa_name], address);
					[addresses addObject:address];
				}
			}
			currentAddress = currentAddress->ifa_next;
		}
	}
	freeifaddrs(interfaces);
	return addresses;
}

@end

Cocoa Tip: Extend NSNumber

As I mentioned in my last post, categories are such a nice addition to Objective-C. They allow me to wrap up new functionality in a standard class without subclassing it myself. Below, I extend NSNumber with two handy methods for printing out numbers and bytes in a human-readable version, turning 1,000,000 into 1 M and 976.6 KB, respectively.

@implementation NSNumber (Utilities)

- (NSString *)humanReadableBase10 {
	if (self == nil) return nil;

	NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
	[formatter setNumberStyle:NSNumberFormatterDecimalStyle];
	[formatter setMaximumFractionDigits:1];

	NSString *formattedString = nil;
	uint64_t size = [self unsignedLongLongValue];
	if (size < 1000) {
		NSString *formattedNumber = [formatter stringFromNumber:[NSNumber numberWithFloat:size]];
		formattedString = [NSString stringWithFormat:@"%@", formattedNumber];
	}
	else if (size < 1000 * 1000) {
		NSString *formattedNumber = [formatter stringFromNumber:[NSNumber numberWithFloat:size / 1000.0]];
		formattedString = [NSString stringWithFormat:@"%@ K", formattedNumber];
	}
	else if (size < 1000 * 1000 * 1000) {
		NSString *formattedNumber = [formatter stringFromNumber:[NSNumber numberWithFloat:size / 1000.0 / 1000.0]];
		formattedString = [NSString stringWithFormat:@"%@ M", formattedNumber];
	}
	else {
		NSString *formattedNumber = [formatter stringFromNumber:[NSNumber numberWithFloat:size / 1000.0 / 1000.0 / 1000.0]];
		formattedString = [NSString stringWithFormat:@"%@ B", formattedNumber];
	}
	[formatter release];

	return formattedString;
}

- (NSString *)humanReadableBase2 {
	if (self == nil)
		return nil;

	NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
	[formatter setNumberStyle:NSNumberFormatterDecimalStyle];
	[formatter setMaximumFractionDigits:1];

	NSString *formattedString = nil;
	uint64_t size = [self unsignedLongLongValue];
	if (size < 1024) {
		NSString *formattedNumber = [formatter stringFromNumber:[NSNumber numberWithFloat:size]];
		formattedString = [NSString stringWithFormat:@"%@ B", formattedNumber];
	}
	else if (size < 1024 * 1024) {
		NSString *formattedNumber = [formatter stringFromNumber:[NSNumber numberWithFloat:size / 1024.0]];
		formattedString = [NSString stringWithFormat:@"%@ KB", formattedNumber];
	}
	else if (size < 1024 * 1024 * 1024) {
		NSString *formattedNumber = [formatter stringFromNumber:[NSNumber numberWithFloat:size / 1024.0 / 1024.0]];
		formattedString = [NSString stringWithFormat:@"%@ MB", formattedNumber];
	}
	else {
		NSString *formattedNumber = [formatter stringFromNumber:[NSNumber numberWithFloat:size / 1024.0 / 1024.0 / 1024.0]];
		formattedString = [NSString stringWithFormat:@"%@ GB", formattedNumber];
	}
	[formatter release];

	return formattedString;
}

@end

UPDATE: I changed the method names to humanReadableBase2 and humanReadableBase10 per Sean’s point.


← Before After →