fill the void

Cocoa Tip: Losing UITableView Selection

In porting Dollar Clock from the iPhone to the iPad, I switched from using a flip view (using UIModalTransitionStyleFlipHorizontal) to a popover view (UIPopoverController). But a strange thing happened: the UITableView in the popover lost its initial selection, after viewDidAppear was called. Regardless of what I did, the row was always deselected, and indexPathForSelectedRow would go from returning the correct path to nil. Turns out I needed to reloadData before assigning it an initial selection.

- (void)viewDidAppear:(BOOL)animated {
	[preferencesListView reloadData]; // This call was necessary for the UITableView to keep its initial selection.
	[preferencesListView selectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] animated:NO scrollPosition:UITableViewScrollPositionNone];
	[self updateInterface];
	[super viewDidAppear:animated];
}

Communicating with a Privileged Tool

In writing Multicast Ping for the Mac, I needed a two-way communication pipe between the interface (parent process) and the privileged tool (child process) and a clean method for terminating the tool through that pipe. NSTask provides setStandardInput and setStandardOutput for just this reason. However, I cannot launch a privileged tool with NSTask; instead, I have to use AuthorizationExecuteWithPrivileges with its lower-level file handle mechanism.

With a bit of help, I implemented a two-way pipe at a high-level and utilize that pipe to cleanly terminate the child process with a simple EOF signal. Apple’s BetterAuthorizationSample sample project touches on this communication pipe but does so at a low-level and only one-way. Matt Gallagher from Cocoa With Love also uses a pipe in invoking other processes when he creates an Open File Killer app, but the pipe is still only used one direction. Thanks to both those tutorials as well as a post from Caius Theory for helping me along. See my GitHub multicast_ping repo for the code in context.

Interface: setup the pipe to the tool

// Execute the command with privileges from SFAuthorizationView.
FILE *handle;
OSErr processError = AuthorizationExecuteWithPrivileges([[authView authorization] authorizationRef], [helperPath UTF8String],
														kAuthorizationFlagDefaults, (char *const *)argv, &handle);
free(argv);
if (processError != 0) {
	NSLog(@"helper tool failed (%d)", processError);
	return NO;
}

// Setup the two-way pipe.
helperHandle = [[NSFileHandle alloc] initWithFileDescriptor:fileno(handle)];
[helperHandle waitForDataInBackgroundAndNotify];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleTaskOutput:) name:NSFileHandleDataAvailableNotification object:helperHandle];

Interface: wait for data from the tool

- (void)handleTaskOutput:(NSNotification *)notification {
	// Get the new data.
	NSFileHandle *handle = (NSFileHandle *)[notification object];
	NSData *data = [handle availableData];
	if ([data length] > 0) {
		// Convert the data into a string.
		NSString *dataString = [[NSString alloc] initWithBytes:[data bytes] length:[data length] encoding:NSUTF8StringEncoding];
		if ([dataString isEqual:@"port in use"]) {
			// Tool failed.
			[self handleToolFailure];
			[dataString release];
			return;
		}

		// Process the string, expecting "address,port,message".
		NSArray *array = [dataString componentsSeparatedByString:@","];
		[dataString release];
		if ([array count] == 3) {
			// Get attributes.
			NSString *address = [array objectAtIndex:0];
			int port = [[array objectAtIndex:1] intValue];
			NSString *message = [array objectAtIndex:2];

			// Create new message.
			Message *newMessage = [[Message alloc] initWithAddress:address andPort:port andMessage:message];
			[messages addObject:newMessage];
			[newMessage release];

			// Update view.
			[listView reloadData];
		}

		// Prepare for more data.
		[handle waitForDataInBackgroundAndNotify];
	}
	else {
		// No data means tool failed.
		[self handleToolFailure];
	}
}

Interface: terminate the tool

// Close pipe to terminate the helper tool.
[helperHandle closeFile];
helperHandle = nil;

Tool: send data to the interface

// Write out the new message to the pipe.
NSString *line = [NSString stringWithFormat:@"%@,%d,%@", sentHost, sentPort, receivedMessage];
[(NSFileHandle *)[NSFileHandle fileHandleWithStandardOutput] writeData:[line dataUsingEncoding:NSUTF8StringEncoding]];

Tool: listen for terminate from the interface

// Wait for parent process to close the pipe. That will send the EOF signal.
[[NSFileHandle fileHandleWithStandardInput] readDataToEndOfFile];

Marketing Tip: Name Your Downloads

Quick, open up your Mac’s Downloads folder or your Windows Desktop. Do you remember where all those files came from? I have fifty items in my Mac’s Downloads right now, and I forget where twenty of them came from. My favorite is “Inst 8.1.184.1.zip”. What was that…

Name your downloads. Think about people downloading files from your website and then remembering them next month. It happens; they’re as busy as you are. Be sure to name those files, so that they remember what the files are and why they cared. Remember, you are a user too.


Xcode Tip: Update Version Numbers with agvtool

Apple includes a handy commmand-line tool called agvtool for updating Xcode project version numbers via Terminal. Run the tool with a build version or marketing version, and it modifies a couple fields:

  • project.pbxproj‘s CURRENT_PROJECT_VERSION
  • Info.plist‘s CFBundleVersion (“Bundle version”)
  • Info.plist‘s CFBundleShortVersionString (“Bundle versions string, short”).
$ agvtool new-version -all "1.2.3.4"
Setting version of project sample to:
    1.2.3.4.
Also setting CFBundleVersion key (assuming it exists)
Updating CFBundleVersion in Info.plist(s)...
Updated CFBundleVersion in "sample.xcodeproj/../sample-Info.plist" to 1.2.3.4

$ agvtool new-marketing-version "1.2.3 (4)"
Setting CFBundleShortVersionString of project sample to:
    1.2.3 (4).
Updating CFBundleShortVersionString in Info.plist(s)...
Updated CFBundleShortVersionString in "sample.xcodeproj/../sample-Info.plist" to 1.2.3 (4)

Google “agvtool” and you’ll inevitably come across two great posts: Apple’s Chris Hanson covering the tool’s goal and Red Sweater’s Daniel Jalkut discussing its automation. They go into much greater detail.

Keep two caveats in mind when using agvtool:

  • Key Existence: agvtool only modifies project.pbxproj and Info.plist where it sees specific keys. It doesn’t add an entry, and CURRENT_PROJECT_VERSION only appears in the project file after you change the value in Versioning->Current Project Version.
  • Key Replacement: agvtool modifies the values for specific keys in these two files. It doesn’t search through the files and replace the keys with the values. So, “${CURRENT_PROJECT_VERSION} Copyright 2010″ doesn’t become “1.2.3.4 Copyright 2010″.

Time is money: Quantify wasted meetings with Dollar Clock

My friend and I were throwing around ideas for iPhone apps, and one of his suggestions was a timer for meetings. Time is money, and we’ve all experienced meetings that waste both. Launch it, and the app starts counting up, displaying the cumulative cost of the meeting. I submitted it to the App Store in February under Dollar Clock as a free app, and it’s had 700 downloads to date.

Dollar Clock is very simple. Just pick the number of people and their pay, and the app starts counting. It remembers when it started, so you can quit the app and relaunch it later. And it tweets, via Tweetie and Twitterrific: “$394 at a 5-person meeting #timeismoney”.




Screenshots