A potential pitfall of CGRectIntegral

This morning while I was checking an app for misaligned elements, I happened upon a misaligned button. (If you’re not using either the iOS Simulator or Instruments to check your app for misaligned images, you should be, but that’s a post for another day.)

Image

Checking the code it was obvious to me where the problem was.

backButton.frame = CGRectMake(5, (navigationBar.bounds.size.height
    - imageBack.size.height)/2, imageBack.size.width,
    imageBack.size.height);

Centering code is especially prone to pixel misalignment. In this case imageBack has a size of (50, 29) while the navigationBar has a height of 44 points. The code above generates a rect with origin = (5, 7.5) and size = (50, 29). So the image ends up vertically misaligned, which in turn makes the child text label inside also misaligned, and hence they show up painted in magenta when the Color Misaligned Images option is checked in the iOS Simulator Debug menu.

This looks like a job for CGRectIntegral, right? But when I change the code to this:

backButton.frame  = CGRectIntegral(CGRectMake(5, 
    (navigationBar.bounds.size.height - imageBack.size.height)/2,
    imageBack.size.width, imageBack.size.height));

I end up with this:

Image

The button is no longer misaligned, but it is now being stretched (hence the yellow wash). Debugging shows that CGRectIntegral has converted the input rect of (5, 7.5) x (50, 29) into (5, 7) x (50, 30). So now the image is being stretched vertically by 1 point. That might be fine for UILabel but not for an image.

The other issue with using CGRectIntegral is that the original rect is actually fine for retina devices because they have 2 pixels per point, so a value of 7.5 actually falls on a pixel boundary, and is the optimal centering for this image. If we adjusted it to origin.y = 7 (without stretching) then it would be 2 pixels closer to the top than to the bottom on a retina device.

I’ve written some helper functions to correctly pixel align rectangles (not point align) for both retina and non-retina screens, and posted them in this gist.

Under non-retina it would convert the rectangle to (5, 7) x (50,29) to pixel align it without stretching, while under retina it would leave the rectangle unmodified at (5, 7.5) x (50, 29).

This finally clears the magenta (alignment) and yellow (stretch) washes from the button:

Image

Addendum

According to the Apple Documentation for CGRectIntegral:

A rectangle with the smallest integer values for its origin and size that contains the source rectangle. That is, given a rectangle with fractional origin or size values, CGRectIntegral rounds the rectangle’s origin downward and its size upward to the nearest whole integers, such that the result contains the original rectangle.

The fractional origin of (5, 7.5) is rounded downward to (5, 7), but I initially thought the size would be left unmodified (not rounded up) because it already comprises 2 whole integers. But that wouldn’t contain the original rectangle, whose lower right corner is positioned at (55, 36.5). In order to contain the original rectangle, the height has to be increased by 1 point from 29 to 30.

Don’t rename your .xcdatamodeld file

Ok, you can rename it but be prepared for weird bugs with Xcode 4.3 (and possibly earlier versions).  In my case I created a new Core Data model file and added it to an existing project.  Then I renamed the model file before attempting to build the project.  Yet even with a clean build and install (deleting the app from the device first), NSManagedObjectModel -initWithContentsOfURL returned nil even though my NSURL pointer was non-nil.

NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"ModelName"
    withExtension:@"momd"];
__managedObjectModel = [[NSManagedObjectModel alloc] 
    initWithContentsOfURL:modelURL];

Short answer: quit and reopen Xcode to fix the problem.

(Solution via Stack Overflow)

Just a black screen on the iPod touch


The other day I ran into this with a universal app. It ran fine on the iPad and on the iPhone, but produced the above when run on an iPod touch. The app would briefly show its splash screen, and then go black. My first thought was that the app had hung during initialization or it had crashed. The truth (as usual) was much simpler. As outlined in this StackOverflow post (don’t look at the answers, look at the author’s own comment on his question), the trouble is that we had specified a nib for both iPhone and iPad but not for iPod touch (this is more likely to happen if you’ve been manually messing around with your app’s plist).

You can fix it by editing the plist directly or by changing the settings under the Info tab of your target’s settings.  If your settings look like this:

Either change “Main nib file base name (iPhone)” to “Main nib file base name” (this will use the same nib for both iPhone and iPod touch) or create a new entry with “Main nib file base name” (this will allow you to use a different nib for iPhone and iPod touch, although I’ve yet to notice an app that has done so).

If you already have a valid entry for “Main nib file base name”, then you have another issue altogether…