Why isn't a registered UIDeviceOrientationDidChangeNotification always called?

4k views Asked by At

I wrote a simple view controller that shows a modal popup dialog. Because it creates a new window to lay over the entire screen, I've added a UIDeviceOrientationDidChangeNotification observer that figures out how to rotate the view using CGAffineTransformMakeRotation(). And it seems to work.

Mostly. The downside is that I can fiddle with the orientation and get it to end up sideways or upside down. Check out this 18s video demonstrating the issue. In debugging, it appears that sometimes the notification doesn't fire, or at least doesn't call the callback method that's listening for it.

I must be missing something, though, because if you look at the video again, you'll notice that the view behind it does rotate properly. That one is managed by -willAnimateRotationToInterfaceOrientation:duration:. How is it that the iOS controller method is always called properly (presumably managed by a UIDeviceOrientationDidChangeNotification observer in UIViewController) but my own code is not?

3

There are 3 answers

3
theory On BEST ANSWER

I fixed this problem by eliminating the use of UIDeviceOrientationDidChangeNotification. What I do instead is create a subclass of UIViewController and make set the custom view to its view. It then simply implements -willAnimateRotationToInterfaceOrientation:duration: to do the rotation. This is actually less finicky code for me. It does feel like a hack, though, to create an arbitrary UIViewController just to take advantage of autorotation in what is essentially a clone of presentModalViewController. Seems like UIDeviceOrientationDidChangeNotification ought to work properly. I expect there is a way to make it behave, but I've yet to find it. :-(

3
dstnbrkr On

UIDeviceOrientation has a few values that don't map to UIInterfaceOrientation:

UIDeviceOrientationUnknown,
UIDeviceOrientationFaceUp,              // Device oriented flat, face up
UIDeviceOrientationFaceDown             // Device oriented flat, face down

Is it possible that the notification callback is firing, but these cases aren't handled?

If the view controller has access to the window - it can forward the didRotate callback to the window manually.

2
Jim Dovey On

I used UIApplicationWillChangeStatusBarOrientationNotification in Kobo, which appears to do the trick. It's a lot more reliable, because you want to match the UI's chosen orientation, not necessarily where the device happens to be. In other words, you can let Apple decide when you should re-orient your window.

Here are a few snippets from the Kobo app's popup dialog code on the iPad (UIAlertView looks kinda dinky on iPad, so Richard Penner wrote a better one). After a few glitches with more recent iOS versions and having to display these dialogs in different situations (all around orientation IIRC), I had to tweak it to sit directly inside the UIWindow, which meant duplicating all the orientation transformation logic.

This code comes from a UIViewController whose view gets dropped straight onto the current window.

- (void) presentDialogWindow
{
    // register for orientation change notification
    [[NSNotificationCenter defaultCenter] addObserver: self
                                             selector: @selector(orientationWillChange:)
                                                 name: UIApplicationWillChangeStatusBarOrientationNotification
                                               object: nil];
    [[NSNotificationCenter defaultCenter] addObserver: self
                                             selector: @selector(orientationDidChange:)
                                                 name: UIApplicationDidChangeStatusBarOrientationNotification
                                               object: nil];
}

- (void) orientationWillChange: (NSNotification *) note
{
    UIInterfaceOrientation current = [[UIApplication sharedApplication] statusBarOrientation];
    UIInterfaceOrientation orientation = [[[note userInfo] objectForKey: UIApplicationStatusBarOrientationUserInfoKey] integerValue];
    if ( [self shouldAutorotateToInterfaceOrientation: orientation] == NO )
        return;

    if ( current == orientation )
        return;

    // direction and angle
    CGFloat angle = 0.0;
    switch ( current )
    {
        case UIInterfaceOrientationPortrait:
        {
            switch ( orientation )
            {
                case UIInterfaceOrientationPortraitUpsideDown:
                    angle = (CGFloat)M_PI;  // 180.0*M_PI/180.0 == M_PI
                    break;
                case UIInterfaceOrientationLandscapeLeft:
                    angle = (CGFloat)(M_PI*-90.0)/180.0;
                    break;
                case UIInterfaceOrientationLandscapeRight:
                    angle = (CGFloat)(M_PI*90.0)/180.0;
                    break;
                default:
                    return;
            }
            break;
        }
        case UIInterfaceOrientationPortraitUpsideDown:
        {
            switch ( orientation )
            {
                case UIInterfaceOrientationPortrait:
                    angle = (CGFloat)M_PI;  // 180.0*M_PI/180.0 == M_PI
                    break;
                case UIInterfaceOrientationLandscapeLeft:
                    angle = (CGFloat)(M_PI*90.0)/180.0;
                    break;
                case UIInterfaceOrientationLandscapeRight:
                    angle = (CGFloat)(M_PI*-90.0)/180.0;
                    break;
                default:
                    return;
            }
            break;
        }
        case UIInterfaceOrientationLandscapeLeft:
        {
            switch ( orientation )
            {
                case UIInterfaceOrientationLandscapeRight:
                    angle = (CGFloat)M_PI;  // 180.0*M_PI/180.0 == M_PI
                    break;
                case UIInterfaceOrientationPortraitUpsideDown:
                    angle = (CGFloat)(M_PI*-90.0)/180.0;
                    break;
                case UIInterfaceOrientationPortrait:
                    angle = (CGFloat)(M_PI*90.0)/180.0;
                    break;
                default:
                    return;
            }
            break;
        }
        case UIInterfaceOrientationLandscapeRight:
        {
            switch ( orientation )
            {
                case UIInterfaceOrientationLandscapeLeft:
                    angle = (CGFloat)M_PI;  // 180.0*M_PI/180.0 == M_PI
                    break;
                case UIInterfaceOrientationPortrait:
                    angle = (CGFloat)(M_PI*-90.0)/180.0;
                    break;
                case UIInterfaceOrientationPortraitUpsideDown:
                    angle = (CGFloat)(M_PI*90.0)/180.0;
                    break;
                default:
                    return;
            }
            break;
        }
    }

    CGAffineTransform rotation = CGAffineTransformMakeRotation( angle );

    [UIView beginAnimations: @"" context: NULL];
    [UIView setAnimationDuration: 0.4];
    self.view.transform = CGAffineTransformConcat(self.view.transform, rotation);
    [UIView commitAnimations];
}

- (void) orientationDidChange: (NSNotification *) note
{
    UIInterfaceOrientation orientation = [[[note userInfo] objectForKey: UIApplicationStatusBarOrientationUserInfoKey] integerValue];
    if ( [self shouldAutorotateToInterfaceOrientation: [[UIApplication sharedApplication] statusBarOrientation]] == NO )
        return;

    self.view.frame = [[UIScreen mainScreen] applicationFrame];

    [self didRotateFromInterfaceOrientation: orientation];
}

We use it by loading this view controller, adding its view to a window, then calling -presentModalDialog, like so:

UIView *window = [[UIApplication sharedApplication] keyWindow];
[window addSubview: theController.view];
[theController presentDialogWindow];

I'm going to make a simple UIViewController subclass to implement the fiddly bits soon, and will post it on my github account. When I do, I'll edit this & link to it.