Device orientation vs interface orientation

Just today I got bit by confusing device orientation and interface orientation. I really should know better. Device orientation is of course the orientation that the device is currently being held in, while interface orientation is the orientation of the running app’s user interface.

What I was trying to do was to hide the status bar while in landscape mode and show it in portrait mode for an iPhone app that operates in the 3 principal orientations: portrait, landscape left, and landscape right.

To achieve that I was using code like this:

#pragma mark - Status Bar

- (BOOL)prefersStatusBarHidden
{
    return (UIDeviceOrientationIsLandscape([[UIDevice currentDevice] orientation]));
}

#pragma mark - Orientation

- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
    [super willAnimateRotationToInterfaceOrientation:toInterfaceOrientation duration:duration];
    
    [self setNeedsStatusBarAppearanceUpdate];
}

This works fine at first blush, especially in the simulator, but not so well in practice. First off, the app doesn’t even support all 4 possible interface orientations (the fourth being upside down portrait), so what happens when the phone is held upside down? Well the interface orientation doesn’t change from its previous orientation (most likely landscape) but the device orientation is not landscape and so the status bar appears. Bug.

But even worse, there are additional device orientations (namely face up and face down) that are neither portrait nor landscape and have no matching interface orientation. If the phone was last in landscape interface orientation and then gets laid flat on the desktop, the device orientation is no longer landscape (it is flat), and so the status bar appears. Bug again.

Just for the record, here’s the definition of UIDeviceOrientation:

typedef NS_ENUM(NSInteger, UIDeviceOrientation) {
    UIDeviceOrientationUnknown,
    UIDeviceOrientationPortrait,            // Device oriented vertically, home button on the bottom
    UIDeviceOrientationPortraitUpsideDown,  // Device oriented vertically, home button on the top
    UIDeviceOrientationLandscapeLeft,       // Device oriented horizontally, home button on the right
    UIDeviceOrientationLandscapeRight,      // Device oriented horizontally, home button on the left
    UIDeviceOrientationFaceUp,              // Device oriented flat, face up
    UIDeviceOrientationFaceDown             // Device oriented flat, face down
};

And here’s the definition for UIInterfaceOrientation:

typedef NS_ENUM(NSInteger, UIInterfaceOrientation) {
    UIInterfaceOrientationPortrait           = UIDeviceOrientationPortrait,
    UIInterfaceOrientationPortraitUpsideDown = UIDeviceOrientationPortraitUpsideDown,
    UIInterfaceOrientationLandscapeLeft      = UIDeviceOrientationLandscapeRight,
    UIInterfaceOrientationLandscapeRight     = UIDeviceOrientationLandscapeLeft
};

It’s interesting that UIInterfaceOrientation is defined in terms of UIDeviceOrientation.

TL;DR;

So what I should have been doing was this:

#pragma mark - Status Bar

- (BOOL)prefersStatusBarHidden
{
    return (UIInterfaceOrientationIsLandscape([[UIApplication sharedApplication] statusBarOrientation]));
}

This works as expected when the device is upside down or flat.

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.

How to Add a Decoration View to a UICollectionView

Decoration views are one of the great new features of collection views.  Unlike cells and supplementary views (e.g. headers and footers), decoration views are layout-driven rather than data-driven.  You can think of them as the shelves in the iBooks Store interface.  Background views are another good candidate for implementing as decoration views.

So how exactly do you add decoration views to your collection view?  It’s actually pretty simple.

  1. Create your decoration view.  It should be a subclass of UICollectionReusableView.
  2. Create your own UICollectionViewLayout subclass if you don’t already have one.  This can be a subclass of UICollectionViewFlowLayout if that’s what you’re using.
  3. Register your decoration view with your layout either by class name or nib.
  4. Return the appropriate attributes for your decoration view in layoutAttributesForElementsInRect:
  5. Implement layoutAttributesForDecorationViewOfKind:atIndexPath: to return attributes for the specified decoration view.

Decoration ViewIn my sample project IntroducingCollectionViews, the first layout, GridLayout, is an iBooks Store-style layout derived from UICollectionViewFlowLayout.  The layout has shelves underneath each row of cells that are implemented as decoration views.  I will go through the steps above to show how I implemented the shelf views in the GridLayout class.

Create the decoration view

I created a ShelfView class derived from UICollectionReusableView.  It’s a very simple view (no subviews) — I just set backgroundColor to a color from a pattern image and add a drop shadow.  (Performance tip: I set the shadowPath for the drop shadow on the backing layer.  See here for why that’s important.)

Shelf

Subclass UICollectionViewLayout

I created GridLayout as a subclass of UICollectionViewFlowLayout.  The superclass handles the heavy lifting of positioning all the cells and headers and footers, but I have to handle the decoration views.

Register the decoration view

In the init method for GridLayout I register the shelf view by class.

[self registerClass:[ShelfView class] forDecorationViewOfKind:
    [ShelfView kind]]

(Where kind is a convenience class method I added to return the kind string. Kind becomes important when you have multiple decoration view classes in your collection view.)

Implement layoutAttributesForElementsInRect:

This method is the meat of a layout.  It’s where the layout tells the collection view about all items (cells, supplementary views, and decoration views) that are to appear within a given rect (generally the current visible rect based on the collection view’s current contentOffset).  The collection view in turn takes over instantiating (or reusing) all necessary views and setting their position, size, and other visible characteristics (alpha, transform, zIndex, etc.).

Because I’m deriving from UICollectionViewFlowLayout, I can just call super to get the attributes for all the cells and supplementary views.  I just need to add the decoration views (if any) to that array.  But in order to know which decoration views might fall within a given rect, I need to do some calculations ahead of time.  To do that I need to override prepareLayout.  This is the method that gets called every time the layout becomes invalidated and is where you can calculate where all the collection elements should go.

In prepareLayout I calculate where the shelves will go (don’t forget to call super).  Unfortunately, this means replicating much of the flow layout positioning since I want to position each shelf directly under each row (line) of speakers (actually each shelf goes under the speaker name label of each SpeakerCell).  I calculate the frame of every shelf rect in the entire content area and store them in a dictionary keyed on index path where section is of course the section the shelf resides in and row is the 0-based index of the shelf within each section moving top to bottom. (This will be important when it’s time to implement layoutAttributesForDecorationViewOfKind:atIndexPath:)

Meanwhile, back in layoutAttributesForElementsInRect: I of course call super and then I enumerate through all my shelf rects and for any shelf rect that intersects with the specified rect (meaning a decoration view should appear at that position), I create a set of layout attributes for that decoration view and add it to the array of attributes returned by super.  To create the attributes I call

[UICollectionViewLayoutAttributes 
     layoutAttributesForDecorationViewOfKind:[ShelfView kind] 
                               withIndexPath:key]

Where I just pass in the kind and the index path (which happens to be the key of the dictionary I’m enumerating through).  Then I set the frame to be the rect I have stored and also set zIndex to 0 (I set zIndex to be 1 on all the cells and supplementary views to make sure they appear in front of the shelves).

Implement layoutAttributesForDecorationViewOfKind:atIndexPath:

This one is pretty easy.  Because I stored my shelf rects in a dictionary keyed on index path, I just use the passed index path to fetch the shelf rect and then I just create the attributes exactly as I did for layoutAttributesForElementsInRect:  (Actually I have yet to see this particular method ever get called.  I have a breakpoint set on it and have never had it be triggered.)

Summary

And that’s what it takes to add decoration views to a collection view.  Decoration views are a cool new feature of collection views and are just one of the improvements of UICollectionView over its predecessor for data display on iOS, UITableView.

Once again, the sample project is on GitHub here. The wood paneling backgrounds are part of a set of retina-ready repeatable assets by Glyphish.

Go forth and make amazing collection view layouts!

MPFlipViewController: a page-flipping container controller


I put a project called MPFlipViewController up on GitHub. It’s a page-flipping container view controller that allows the user to flip through a series of view controllers as if they were pages in a book. It is based on the MPFlipTransition class I already have on GitHub, but instead of being just a transition between 2 views, it is a full-fledged container view controller that supports panning and swiping between pages (child view controllers). It follows the Containment API introduced in iOS 5, so it behaves as a proper container view controller similar to the system container controllers (e.g. UINavigationController, UITabBarController and UIPageViewController).

Requirements

Xcode 4.3
iOS 5
ARC

API

The API is based on the API for UIPageViewController (since they fulfill an almost identical role). There is a data source protocol that you implement to specify the previous and next pages, and a delegate protocol that you implement in order to receive feedback on whether or not a page-turn operation completed and also to optionally specify the new orientation in the event that device orientation changes (i.e. user rotates the device).

Use

To create a flip controller use the initWithOrientation: method and pass in the desired orientation (horizontal or vertical):

- (id)initWithOrientation:(MPFlipViewControllerOrientation)orientation;


To set the content use the setViewController:direction:animated:completion: method where direction indicates whether the animation should be a page flip forward or backward.

- (void)setViewController:(UIViewController *)viewController 
                direction:(MPFlipViewControllerDirection)direction 
                 animated:(BOOL)animated 
               completion:(void (^)(BOOL finished))completion;

To enable touch gestures (panning and swiping between pages) implement the MPFlipViewControllerDataSource delegate to provide the previous and next pages (if any). Return nil for either method to indicate the user is already on the first or last page.

- (UIViewController *)flipViewController:
          (MPFlipViewController *)flipViewController
      viewControllerBeforeViewController:
          (UIViewController *)viewController;

- (UIViewController *)flipViewController:
          (MPFlipViewController *)flipViewController
       viewControllerAfterViewController:
          (UIViewController *)viewController;

To be notified of whether a page turn animation completed or not, set the MPFlipViewControllerDelegate and implement the optional flipViewController:didFinishAnimating:previousViewController:transitionCompleted: method. This method is only called if the page turn was gesture-driven (i.e. in response to a pan or swipe), and not programmatic (i.e. in response to a call to setViewController:direction:animated:completion:).

- (void)flipViewController:(MPFlipViewController *)flipViewController 
        didFinishAnimating:(BOOL)finished 
    previousViewController:(UIViewController *)previousViewController 
       transitionCompleted:(BOOL)completed;

To change the orientation of the flip controller when device orientation changes, set the MPFlipViewControllerDelegate and implement the optional flipViewController:orientationForInterfaceOrientation: method and return the desired orientation.

- (MPFlipViewControllerOrientation)flipViewController:
                       (MPFlipViewController *)flipViewController 
                   orientationForInterfaceOrientation:
                       (UIInterfaceOrientation)orientation;

Demo Project

The GitHub project includes a sample project that demonstrates the use of the control and its API.

MPFlipTransition – add flip transitions to your app

I’ve added flip transitions to my MPFoldTransition project on GitHub. It provides a class you can use to add a page-flipping transition to your application in just a single line of code (in most cases).

Update: For a touch gesture-enabled container controller with page-flipping (not just a transition), see MPFlipViewController.

Features

There are 3 style bits that can be combined to create 8 different animations.

Direction

Controls whether the page flips from right to left (Forward) or left to right (Backward).

Orientation

Sets whether the page flip is horizontal or vertical.

Perspective

Determines whether the page flips towards the user (Normal) or away from the user (Reverse).

Present a modal view controller

There are extension methods to UIViewController to present or dismiss modal view controllers using flip transitions:

- (void)presentViewController:(UIViewController *)viewControllerToPresent
                    flipStyle:(MPFlipStyle)style 
                   completion:(void (^)(BOOL finished))completion;

- (void)dismissViewControllerWithFlipStyle:(MPFlipStyle)style 
                                completion:(void (^)(BOOL finished))completion;

From your UIViewController subclass you would call this to present your modal view controller:

[self presentViewController:modalViewController
                  flipStyle:MPFlipStyleDefault 
                 completion:nil];

And then call this to dismiss it:

[self dismissViewControllerWithFlipStyle:MPFlipStyleDirectionBackward 
                              completion:nil];

Tip: dismiss your modal controller using a style with the opposite direction bit (Forward or Backward), so that you get the reverse animation.

Push a view controller onto a navigation stack

There are extension methods to UINavigationController to push or pop a view controller using flip transitions:

- (void)pushViewController:(UIViewController *)viewController
                 flipStyle:(MPFlipStyle)style;

- (UIViewController *)popViewControllerWithFlipStyle:
    (MPFlipStyle)style;

From your UIViewController subclass you would call this to push a new view controller onto the stack:

[self.navigationController pushViewController:detailViewController
                                    flipStyle:MPFlipStyleDefault];

And then call this to pop it back off:

[self.navigationController popViewControllerWithFlipStyle:MPFlipStyleDirectionBackward];

Tip: pop your view controller using a style with the opposite direction bit (Forward or Backward) from the style used to push it onto the stack, so that you get the reverse animation.

Transition between any 2 views or controllers

MPFlipTransition has class methods for generic view and view controller transitions:

+ (void)transitionFromViewController:(UIViewController *)fromController
                    toViewController:(UIViewController *)toController
                            duration:(NSTimeInterval)duration
                               style:(MPFlipStyle)style
                          completion:(void (^)(BOOL finished))completion;

+ (void)transitionFromView:(UIView *)fromView
                   toView:(UIView *)toView
                 duration:(NSTimeInterval)duration
                    style:(MPFlipStyle)style
         transitionAction:(MPTransitionAction)action
               completion:(void (^)(BOOL finished))completion;

If you really need to get under the hood (e.g. to adjust the timing curve, shadow effects, or skew), you can initialize your own instance of MPFlipTransition, set the properties as desired, and then call the perform: method to execute the transition.

Storyboard support

You can even incorporate modal or navigation stack flip segues without writing a single line of code! Simply use 1 of the 3 custom UIStoryboardSegue subclasses that are included. These cover modal presentation and push/pop to a navigation stack. Just create a segue between 2 controllers in your storyboard, select Custom as the segue type, then enter MPFlipModalSegue, MPFlipNavPushSegue, or MPFlipNavPopSegue as the Segue Class.

Demo project

The GitHub project includes a sample project that demonstrates the use of all the different API’s as well as all of the transition styles.

iOS version

iOS 5-only because I wrote it with ARC and included storyboard support.

MPFoldTransition – add fold transitions to your app

I’ve put a project I call MPFoldTransition up on GitHub. It provides a class you can use to add a fold transition to your application in just a single line of code (in most cases).
What is a fold transition? It’s an animation such as shown above (or as popularized by the Clear todo list app) where an object folds in upon itself until it disappears and the content surrounding it moves in to fill the gap. Except in this case, I’m doing it for an entire UIView or UIViewController and not just a single UITableViewCell. (For an in-depth analysis of what goes into making a folding animation, read my post here.)

Update: The GitHub project now includes flip transitions as well!

Update 2: The folding effect has been improved with gradient shadows.

Update 3: For a touch gesture-enabled container controller with page-flipping (not just a transition), see MPFlipViewController.

Features

There are 3 style bits that can be combined to create 8 different animations.

Direction

Controls whether the current view collapses inward (Fold) or if the next view expands outward (Unfold).

Mode

Determines whether the next view slides in flat (Normal) or rotates in at right angles to the collapsing halves of the previous view (Cubic).

Orientation

Sets whether the fold is vertical or horizontal.

Present a modal view controller

There are extension methods to UIViewController to present or dismiss modal view controllers using fold transitions:

- (void)presentViewController:(UIViewController *)viewControllerToPresent
                    foldStyle:(MPFoldStyle)style 
                   completion:(void (^)(BOOL finished))completion;

- (void)dismissViewControllerWithFoldStyle:(MPFoldStyle)style 
                                completion:(void (^)(BOOL finished))completion;

From your UIViewController subclass you would call this to present your modal view controller:

[self presentViewController:modalViewController
                  foldStyle:MPFoldStyleDefault 
                 completion:nil];

And then call this to dismiss it:

[self dismissViewControllerWithFoldStyle:MPFoldStyleUnfold 
                              completion:nil];

Tip: dismiss your modal controller using a style with the opposite fold bit (Fold or Unfold), so that you get the reverse animation.

Push a view controller onto a navigation stack

There are extension methods to UINavigationController to push or pop a view controller using fold transitions:

- (void)pushViewController:(UIViewController *)viewController
                 foldStyle:(MPFoldStyle)style;

- (UIViewController *)popViewControllerWithFoldStyle:
    (MPFoldStyle)style;

From your UIViewController subclass you would call this to push a new view controller onto the stack:

[self.navigationController pushViewController:detailViewController
                                    foldStyle:MPFoldStyleDefault];

And then call this to pop it back off:

[self.navigationController popViewControllerWithFoldStyle:MPFoldStyleUnfold];

Tip: pop your view controller using a style with the opposite fold bit (Fold or Unfold) from the style used to push it onto the stack, so that you get the reverse animation.

Transition between any 2 views or controllers

MPFoldTransition has class methods for generic view and view controller transitions:

+ (void)transitionFromViewController:(UIViewController *)fromController
                    toViewController:(UIViewController *)toController
                            duration:(NSTimeInterval)duration
                               style:(MPFoldStyle)style
                          completion:(void (^)(BOOL finished))completion;

+ (void)transitionFromView:(UIView *)fromView
                   toView:(UIView *)toView
                 duration:(NSTimeInterval)duration
                    style:(MPFoldStyle)style
         transitionAction:(MPTransitionAction)action
               completion:(void (^)(BOOL finished))completion;

If you really need to get under the hood (e.g. to adjust the timing curve), you can initialize your own instance of MPFoldTransition, set the properties as desired, and then call the perform method to execute the transition.

Storyboard support

You can even incorporate modal or navigation stack fold segues without writing a single line of code! Simply use 1 of the 3 custom UIStoryboardSegue subclasses that are included. These cover modal presentation and push/pop to a navigation stack. Just create a segue between 2 controllers in your storyboard, select Custom as the segue type, then enter MPFoldModalSegue, MPFoldNavPushSegue, or MPFoldNavPopSegue as the Segue Class.

Demo project

The GitHub project includes a sample project that demonstrates the use of all the different API’s as well as all of the transition styles.

iOS version

iOS 5-only for now because I wrote it with ARC and included storyboard support.