Aligned UIViews

The iOS Simulator has some nice debugging options. One of the is the Color Misaligned Images option.

According to the documentation:

Places a magenta overlay over images whose bounds are not aligned to the destination pixels. If there is not a magenta overlay, places yellow overlay over images drawn with a scale factor.

I feel that this option is not documented enough. At least not in one place. What does it mean to be not aligned to destination pixels? What happens when the images are misaligned? How to fix that?

My understanding is that misaligned images are those images which have their self.frame.origin.x or self.frame.origin.y not integral. This also propagates to your subviews, so if a view is marked as misaligned, you have to check the whole view hierarchy up until UIWindow.

When the images are misaligned the antialiasing triggers. Unlike in your computer games, you don't want antialiasing in your iOS App. When the antialiasing triggers, the device stops to render image pixels by perfectly mapping them to device pixels. It starts to use an algorithm to make device pixels display a mash-up of nearby image pixels, as the image doesn't fit perfectly in the view's bounds.

This makes the images blurry and not sharp and makes your App look bad. Misaligned UILabel views looks especially bad, as the text on them becomes blurry and you have a feeling that you forgot your glasses to work today. After some time working on this you can start to spot misaligned images in other Apps in the AppStore too.

Bad solution

There are some resources on the CGRectIntegral function to fix that. According to the documentation:

Returns the smallest rectangle that results from converting the source rectangle values to integers.

It looks like it solves the problem, but this function generates more problems than it solves, especially if you are doing custom layout code.

Imagine a layout of buttons self.buttonsArray and some container view that layouts them equally distributed in -(void)layoutSubviews method:

- (void)layoutSubviews {
    [super layoutSubviews];
    CGFloat space = self.frame.size.width / self.buttonsArray.count;
    [self.buttonsArray enumerateObjectsUsingBlock:^(UIButton *button, NSUInteger idx, BOOL *stop){
    	CGRect frame = button.frame;
        frame.origin.x = idx * space;
        button.frame = CGRectIntegral(frame);
    }
}

Let's also assume that:

  • Container width is equal to 100.0.
  • self.buttonsArray has 3 buttons of width 33.

and we know that in general -(void)layoutSubviews should be idempotent, because it gets called a lot.

The table ilustrates the (x, width) coordinates of frames of buttons, during subsequent layoutSubviews calls.

Start Call #1 Call #2 Call #3
Button #1 (0, 33) (0, 33) (0, 33) (0, 33)
Button #2 (0, 33) (34, 32) (34, 31) (34, 30)
Button #3 (0, 33) (67, 32) (67, 31) (67, 30)

As you can see, in every call of layoutSubviews the width is decreased by 1 for all the buttons except the first. This usually has a humorous value to the QA department as every time we relayout the buttons get smaller. To understand this we have to imagine what would happen without CGRectIntegral.

The second one and the third one would be misaligned because would be layouted at 33.333 and 66.666 respectively. As they method returns the smallest rectangle fitting a rectangle with x == 33.333 and width == 33 it can't be a rectangle with the same width so the returned rectangle is smaller, with width == 32. In every subsequent call we encounter the problem of fitting the rectangle an we decrease the size of the second and the third rectangle.

Good solution

Instead of CGRectIntegral I use a simple category on UIView with one method -(void)align.

@implementation UIView (Align)

- (void)align {
    self.frame = CGRectMake(ceilf(self.frame.origin.x),
        ceilf(self.frame.origin.y),
        ceilf(self.frame.size.width),
        ceilf(self.frame.size.height));
}
  • ceilf is a function that returns a mathematical ceiling of a float value.
  • By just using the ceilf function we are not trying to fit anything in anything, we just operate on values of the coordinates. There's no way that the view will grow or shrink because of continous relayout.