objective-ciosautoresizingmaskuistatusbar

Slide all views up/down as statusBar is hidden/shown


I’m letting the user show/hide the statusBar at will, and I want all the views to slide down/up with it. I assumed setting the autoresizing mask would take care of this. I’ve added the navigation controller programmatically, so I did this:

[self.view setAutoresizingMask:UIViewAutoresizingFlexibleHeight];
[self.navigationController.view setAutoresizingMask:UIViewAutoresizingFlexibleHeight];
[self.navigationController.navigationBar setAutoresizingMask:UIViewAutoresizingFlexibleTopMargin];

This has no effect.

I printed the frame rects of self.view and self.navigationController.view before and after hiding the statusBar, and they remain exactly the same height.

Since autoresizesSubviews defaults to YES, I doubt that is the problem. I must not be setting the autoresizing mask correctly. Does anybody know what I'm doing wrong?


Solution

  • The answer seems to be that the container view's autoresizing mask is simply not coordinated with the status bar. There’s no choice but to write code adjusting the layout.

    Since Apple didn’t provide automatic coordination between the statusBar and other elements, you’d think they’d let us do it ourselves. But, no, we are not permitted to set the statusBar frame directly. The only way to animate statusBar repositioning is via UIStatusBarAnimationSlide, which uses its own timeline and will never match other animations.

    However, we can control the timing of a statusBar fade and slide the container view along with it. The subviews' autoresize masks will do the rest. This actually looks pretty good, and the coding, although complicated by some weird framing behavior, is not too painful:

    UIApplication *appShared = [UIApplication sharedApplication];
    CGFloat fStatusBarHeight = appShared.statusBarFrame.size.height; // best to get this once and store it as a property
    BOOL bHideStatusBarNow = !appShared.statusBarHidden;
    CGFloat fStatusBarHeight = appDelegate.fStatusBarHeight;
    if (bHideStatusBarNow)
        fStatusBarHeight *= -1;
    
    CGRect rectView = self.view.frame; // must work with frame; changing the bounds won't have any effect
    
    // Determine the container view's new frame.
    // (The subviews will autoresize.)
    switch (self.interfaceOrientation) {
        case UIInterfaceOrientationPortrait:
        case UIInterfaceOrientationPortraitUpsideDown: 
            rectView.origin.y += fStatusBarHeight;
            rectView.size.height -= fStatusBarHeight;
            break;
        case UIInterfaceOrientationLandscapeLeft:
        case UIInterfaceOrientationLandscapeRight:
            // In landscape, the view's frame will sometimes complement the orientation and sometimes not. 
            /*
             Specifically, if view is loaded in landscape, its frame will reflect that; otherwise, the frame will always be in portrait orientation.
             This is an issue only when the navBar is present. Regular view controllers can count on the frame staying in portrait orientation no matter what.
             But regular view controllers have another oddity: In the upside-down orientations, you should adjust the height only; the origin takes care of itself.
             */
            if (rectView.size.width < rectView.size.height) {
                rectView.origin.x += fStatusBarHeight;
                rectView.size.width -= fStatusBarHeight;
            }
            else {
                rectView.origin.y += fStatusBarHeight;
                rectView.size.height -= fStatusBarHeight;
            }
            break;
        default:
            break;
    }
    
    // The navBar must also be explicitly moved.
    CGRect rectNavBar = [self.navigationController.navigationBar frame];
    rectNavBar.origin = CGPointMake(0.0f, rectNavBar.origin.y + fStatusBarHeight);
    
    // Perform the animated toggling and reframing.
    [UIView animateWithDuration:kAnimDurationToggleStatusBar animations:^{
        [appShared setStatusBarHidden:bHideStatusBarNow]; // you can add withAnimation:UIStatusBarAnimationSlide here and it will work, but the timing won't match
        [self.navigationController.navigationBar setFrame:rectNavBar];
        [self.view setFrame:rectView];
    }];
    

    There’s no need to do anything to the toolbar, which remains glued to the bottom of the screen -- as long as you have not set the window frame to mainScreen.bounds.

    One snag is how to get the statusBar height when you want to re-display it, since statusBarFrame returns a 0-area rect if it is currently hidden. It turns out that doing a preliminary show/hide, without animation, just to get the rect, works fine. There’s no visible flash.

    Also, if you are using a xib/nib, be sure that its view's statusBar is set to None.

    Maybe some day Apple will enhance the statusBar layout behavior. Then all this code, for every view controller, will have to be redone...