Search code examples
iosuinavigationcontrollersize-classesuitraitcollection

Navigation pop animation unexpected - jumps off screen


I'm having an issue where my UINavigationController's default pop animation has unexpected behavior - the popped controller sometimes jumps off-screen left or right.

The issue seems related to overriding the controller's UITraitCollection.

I have a universal app, and on iPad, a custom UIPresentationController to display a nav in a partial modal, where its width is a fraction of the screen width. Thus, I override the horizontalSizeClass to compact on the UIPresentationController's overrideTraitCollection property, so all controllers presented in this "half modal" assume their iPhone layout.

Overriding that size class seems to trigger the bug. Suddenly, when popping a controller within that "half modal", the animation is messed up in landscape (it either jumps left or right).

Here's an example of what it looks like: unexpected-pop-animation

Attempts

First, when I get rid of the traitCollection override, the bug goes away. Obviously though, I want to override the horizontal size class, because these views are reused elsewhere in regular environments too.

Thus, I tried overriding the horizontalSizeClass of the modal's children in other ways, like:

  • Using the modal Nav's UINavigationControllerDelegate to override each child's traitCollection on navigationController:didShowViewController:animated: – this seemed to make no difference
  • Having the first nav child override the secondary child's traitCollection before it pushes it

Like so:

[self.navigationController setOverrideTraitCollection:compactTraitCollection forChildViewController:secondaryController];
[self.navigationController pushViewController:secondaryController animated:YES];

Interestingly, this fixes the pop animation bug, but then my primary controller (self) is still in a Regular horizontalSizeClass... Furthermore, this seems like bad practice. My view controllers shouldn't need to know anything about their presentation! That should be handled by the UIPresentationController, and seems supported by the fact that the presentation controller has a overrideTraitCollection property.


Solution

  • Turns out the culprit was the implementation of supportedInterfaceOrientations, relying on size classes:

    - (UIInterfaceOrientationMask)supportedInterfaceOrientations
    {
        // Don't do this if you ever override size classes
        if (self.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassRegular)
        {
            return UIInterfaceOrientationMaskAll;
        }
        return UIInterfaceOrientationMaskPortrait;
    }
    

    Because the horizontalSizeClass of the "half modal" controllers was overridden to use UIUserInterfaceSizeClassCompact, they were assuming a Portrait-only orientation. The navigation controller didn't know how to handle that.

    The solution:

    Changing the above code to rely on device type fixes the issue:

    - (UIInterfaceOrientationMask)supportedInterfaceOrientations
    {
        // Basing off of size classes causes unexpected behavior when overriding size classes - use interface idiom instead
        if (self.traitCollection.userInterfaceIdiom == UIUserInterfaceIdiomPhone)
        {
            return UIInterfaceOrientationMaskAll;
        }
        return UIInterfaceOrientationMaskPortrait;
    }
    

    This probably should have been the way to go in the first place, but given Apple's encouragement of being device-agnostic and only relying on size classes, it wasn't what I did.

    Anyway, for prosperity, here's the test project I used to debug this: https://github.com/bradgmueller/half-modal-test