fill the void

Codesigning on Different OS Versions

A couple days ago, I was trying to codesign the latest application bundle when I got a very strange error message:

bdunagan$ codesign -f -s 'EMC Corporation' 'Retrospect.app'
codesign_allocate: for architecture i386 object:
/Retrospect.app/Contents/MacOS/Retrospect malformed object (unknown load command 5)
Retrospect.app: object file format invalid or unsuitable



Googling for those exact phrases lead nowhere. Eventually, I came across an excellent StackOverflow comment from Apple employee Chris Espinosa: different OSes. In fact, that was the problem. The application bundle was built on 10.6 and I was trying to use codesign on 10.5. Doesn’t work.


Create iTunes Link Arrows

iTunes has a great user interface affordance for adding actions to text: clickable arrows embedded right next to the text. (Perhaps Apple wanted to avoid an interface filled with blue, underlined text.) I haven’t found any formal name for them in Apple’s HIG, so I call them link arrows (or jump arrows). Sadly, this is my third post on recreating link arrows. My first post was a first pass at the problem, whereas my second post looked a bit better but not great.

This time, the UI looks right, and clicking works as expected. I wrote up a small sample app to demo the link arrows; see Google Code.







The key is tracking. In the previous post, I used NSCell::hitTestForEvent to detect whether a click “hit” the link arrow image on a mouse down event, but then I immediately acted on it, without waiting for a mouse up event. Jarring and wrong.

I need to track what happens after that initial NSLeftMouseDown event. For this, I use NSCell::trackMouse to track all relevant mouse events until the next NSLeftMouseUp event. Thanks to Apple’s PhotoSearch sample app and Rowan Beentje’s Sequel Pro code for this much needed direction.

There are a couple corner cases I handle in trackMouse:

  • mouse down outside => mouse up inside (no click)
  • mouse down inside => mouse up outside (no click)
  • mouse down inside => mouse drag out => mouse drag in => mouse up inside (click)

These mouse event sequences are what people expect from buttons, and I want people to think of the link arrows as buttons, as iTunes treats them.

Below is the main code from LinkArrowCell.m. The rest is just scaffolding.

// snipped from LinkArrows/LinkArrowCell.m (MIT license)

- (void)drawInteriorWithFrame:(NSRect)aRect inView:(NSView *)controlView {
	NSRect textRect = NSMakeRect(aRect.origin.x, aRect.origin.y, aRect.size.width - kLinkArrowWidth - kLinkArrowWidthPadding, aRect.size.height);

	// Draw text.
	[super drawInteriorWithFrame:textRect inView:controlView];

	// Draw link arrow.
	if ([self shouldDisplayLink]) {
		if (![self isHighlighted]) {
			// The cell is not highlighted.
			[linkArrow setImage:[NSImage imageNamed:@"link_arrow"]];
			[linkArrow setAlternateImage:[NSImage imageNamed:@"link_arrow_click"]];
		}
		else if ([[[self controlView] window] isKeyWindow] && [[[self controlView] window] firstResponder] == [self controlView]) {
			// The cell is highlighted, and the window is key.
			[linkArrow setImage:[NSImage imageNamed:@"link_arrow_highlight"]];
			[linkArrow setAlternateImage:[NSImage imageNamed:@"link_arrow_click_highlight"]];
		}
		else {
			// The cell is highlighted, but the window is not key.
			[linkArrow setImage:[NSImage imageNamed:@"link_arrow_click"]];
			[linkArrow setAlternateImage:[NSImage imageNamed:@"link_arrow"]];
		}

		[linkArrow drawInteriorWithFrame:[LinkArrowCell linkRect:aRect] inView:controlView];
	}
}

- (NSUInteger)hitTestForEvent:(NSEvent *)event inRect:(NSRect)cellFrame ofView:(NSView *)controlView {
	// Figure out hit point in view.
	NSRect linkRect = [LinkArrowCell linkRect:cellFrame];
	NSPoint p = [[[NSApp  mainWindow] contentView] convertPoint:[event locationInWindow] toView:controlView];
	if (p.x > linkRect.origin.x && p.x < (linkRect.origin.x + linkRect.size.width) &&
		p.y > linkRect.origin.y && p.y < (linkRect.origin.y + linkRect.size.height)) {
		// Point inside link arrow.
		[linkArrow setState:NSOnState];
		return NSCellHitContentArea | NSCellHitTrackableArea;
	}
	else {
		// Point outside link arrow.
		[linkArrow setState:NSOffState];
		return [super hitTestForEvent:event inRect:cellFrame ofView:controlView];
	}
}

- (BOOL)trackMouse:(NSEvent *)event inRect:(NSRect)cellFrame ofView:(NSView *)controlView untilMouseUp:(BOOL)untilMouseUp {
	// Check if link arrow was hit.
	int hitType = [self hitTestForEvent:[NSApp currentEvent] inRect:cellFrame ofView:controlView];
	BOOL isButtonClicked = hitType == (NSCellHitContentArea | NSCellHitTrackableArea);
	if (!isButtonClicked) return YES;

	// Ignore events other than mouse down.
	if ([event type] != NSLeftMouseDown) return YES;

	// Grab all events until a mouse up event.
	while ([event type] != NSLeftMouseUp) {
		// Check if link arrow was hit.
		hitType = [self hitTestForEvent:[NSApp currentEvent] inRect:cellFrame ofView:controlView];
		isButtonClicked = hitType == (NSCellHitContentArea | NSCellHitTrackableArea);
		// Update the cell's display.
		[controlView setNeedsDisplayInRect:cellFrame];
		// Pass event if not hit.
		if (!isButtonClicked) {
			[NSApp sendEvent:event];
		}
		// Grab next event.
		event = [[controlView window] nextEventMatchingMask:(NSLeftMouseUpMask | NSLeftMouseDraggedMask | NSMouseEnteredMask | NSMouseExitedMask)];
	}

	// Perform click only if link arrow was hit.
	if (isButtonClicked) {
		[self performClick:nil];
	}

	return YES;
}

Before I went with the trackMouse approach, I looked at subclassing NSCell and overriding startTrackingAt, continueTracking, stopTracking. For some reason, my overridden methods never triggered. Very odd as Apple’s NSCell docs bring up the approach, and many developers refer to it working on various email lists like cocoa-dev. Still not sure why this path didn’t work, but trackMouse certainly does.

Again, a sample project for this code is at Google Code.


Cocoa Tip: NSApp‘s currentEvent

Let’s say I want to detect a modifier key. This information is embedded in every NSEvent object as flags in the modifierFlags attribute. But what if my current method isn’t passed an event? The answer is not NSApp‘s nextEventMatchingMask. I explored that option for too long before moving on. The answer is NSApp‘s currentEvent. With it, I have easy access to the current event that the application is dealing with. To detect the modifier keys, I simply extract the modifierFlags attribute and check it for them.

// Detect the modifier flags (keys) from the current NSEvent.
NSEvent *event = [NSApp currentEvent];
if (event != nil) {
	BOOL isCommandKey = ([event modifierFlags] & NSCommandKeyMask) != 0;
	BOOL isShiftKey = ([event modifierFlags] & NSShiftKeyMask) != 0;
}

Cocoa Tip: Continuous Updates and Bindings

Today, I was looking at a UI bug where the value of an NSTextField, populated through Cocoa Bindings, wasn’t getting saved. I tabbed over to the field, changed the value, tabbed over to the blue “Done” button and hit Space. Value saved. No bug. So I tried following the steps to reproduce exactly. Click in the field; change the value; click “Done”. Ah, the value didn’t save. Turns out, bindings don’t fire by default for an NSTextField unless it loses focus. To get the bindings to fire, I had to go into Interface Builder and check “Continuously Updates Value” (see the screenshot). With that checked, the bindings fired for both NSControlTextDidEndEditingNotification and NSControlTextDidChangeNotification notifications.


iPhone Tip: Rotating UIView

The iPhone OS’s accelerometer tells me the exact x, y, and z orientation of an iPhone or iPod Touch. Which is freakin’ awesome. To demonstrate how easy it is to leverage, I wrote a bit of code to rotate a UILabel in a simple Utility app. The controller sets itself as the delegate for UIAccelerometer and then responds to the selector accelerometer:didAccelerate:. I started by simply using the exact rotation, but that resulted in a very wobbly text label; so, I added a bit of rounding and animation to dampen it.

// add UIAccelerometerDelegate as a delegate to the controller
// add a UILabel to the XIB and hook it up to the controller
// add QuartzCore framework

// MainViewController.h
@interface MainViewController : UIViewController <UIAccelerometerDelegate> {
	IBOutlet UILabel *textLabel;
}
@end

// MainViewController.m
#import "MainViewController.h"
#import <QuartzCore/QuartzCore.h>

@implementation MainViewController

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
    if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) {
		// Set this controller as the accelerometer's delegate.
		[[UIAccelerometer sharedAccelerometer] setDelegate:self];
    }
    return self;
}

- (void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration {
	// Get the rotation in radians.
	CGFloat rotation = (atan2(acceleration.x, acceleration.y) + M_PI);
	// Dampen it a bit.
	float div = 48 / (2 * M_PI);
	rotation = (int)(rotation * div) / div;
	// Animate the movement a bit.
	[UIView beginAnimations:nil context:NULL];
	[UIView setAnimationDuration:.2];
	textLabel.layer.transform = CATransform3DMakeRotation(rotation, 0, 0, 1);
	[UIView commitAnimations];
}

@end