Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

iOS: navigation bar's titleView doesn't resize correctly when phone rotates

I'm writing an iPhone app that (like most apps) supports auto-rotation: You rotate your phone, and its views rotate and resize appropriately.

But I am assigning a custom view to navigationItem.titleView (the title area of the navigation bar), and I can't get that view to resize correctly when the phone rotates.

I know what you're thinking, "Just set its autoresizingMask to UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight," but it's not that simple. Of course, if I don't set my view's autoresizingMask, then my view doesn't resize; and I want it to resize.

The problem is, if I do set its autoresizingMask, then it resizes correctly as long as that view is visible; but the titleView's size gets messed up in this scenario:

  1. Run the app, with the phone held in portrait mode. Everything looks good.
  2. Do something that causes the app to push another view onto the navigation stack. E.g. click a table row or button that causes a call to [self.navigationController pushViewController:someOtherViewController animated:YES].
  3. While viewing the child controller, rotate the phone to landscape.
  4. Click the "Back" button to return to the top-level view. At this point, the title view is messed up: Although you are holding the phone in landscape mode, the title view is still sized as if you were holding it in portrait mode.
  5. Finally, rotate the phone back to portrait mode. Now things get even worse: The title view shrinks in size (since the navigation bar got smaller), but since it was already too small, now it is much too small.

If you want to reproduce this yourself, follow these steps (this is a bit of work):

  1. Make an app using Xcode's "Navigation-based Application" wizard.
  2. Set it up so that the top-level table view has rows that, when you click them, push a detail view onto the navigation stack.
  3. Include this code in both the top-level view controller and the detail view controller:

    - (BOOL)shouldAutorotateToInterfaceOrientation:
            (UIInterfaceOrientation)interfaceOrientation {
        return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown);
    }
    
  4. Include this code in only the top-level view controller:

    - (void)viewDidLoad {
        [super viewDidLoad];
    
        // Create "Back" button
        UIBarButtonItem *backButton = [[UIBarButtonItem alloc] initWithTitle:@"Master"
            style:UIBarButtonItemStylePlain target:nil action:nil];
        self.navigationItem.backBarButtonItem = backButton;
        [backButton release];
    
        // Create title view
        UILabel* titleView = [[[UILabel alloc] initWithFrame:CGRectMake(0,0,500,38)] autorelease];
        titleView.textAlignment = UITextAlignmentCenter;
        titleView.text = @"Watch this title view";
    
        // If I leave the following line turned on, then resizing of the title view
        // messes up if I:
        //
        // 1. Start at the master view (which uses this title view) in portrait
        // 2. Navigate to the detail view
        // 3. Rotate the phone to landscape
        // 4. Navigate back to the master view
        // 5. Rotate the phone back to portrait
        //
        // On the other hand, if I remove the following line, then I get a different
        // problem: The title view doesn't resize as I want it to when I:
        //
        // 1. Start at the master view (which uses this title view) in portrait
        // 2. Rotate the phone to landscape
        titleView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    
        self.navigationItem.titleView = titleView;
    }
    
  5. Finally, follow my repro steps.

So ... am I doing something wrong? Is there a way to make my titleView always resize correctly?

like image 685
Mike Morearty Avatar asked Jan 14 '11 05:01

Mike Morearty


6 Answers

You should also set the contentMode of the UIImageView to get the titleView properly displayed in landscape and/or portrait mode :

imgView.contentMode=UIViewContentModeScaleAspectFit;

The whole sequence: (self is a UIViewController instance)

UIImageView* imgView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"myCustomTitle.png"]];
imgView.autoresizingMask=UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
imgView.contentMode=UIViewContentModeScaleAspectFit;
self.navigationItem.titleView = imgView;
[imgView release];
like image 154
yonel Avatar answered Oct 31 '22 17:10

yonel


I had something similar - but it was returning (popping) to root view controller. Ultimately, I went with the following for popping:

[[self navigationController] setNavigationBarHidden:YES animated:NO];
[[self navigationController] popViewControllerAnimated:YES];
[[self navigationController] setNavigationBarHidden:NO animated:NO];

And it worked. There may have been a better way but - after all the hours I'd already spent on this issue - this was good enough for me.

like image 22
westsider Avatar answered Oct 31 '22 18:10

westsider


I dealt with this same issue by keeping track of the customView's initial frame, then toggling between that and a scaled CGRect of the initial frame in a -setLandscape method on a UIButton subclass. I used the UIButton subclass as navigationItem.titleView and navigationItem.rightBarButtonItem.

In UIButton subclass -

- (void)setLandscape:(BOOL)value
 {
     isLandscape = value;

     CGFloat navbarPortraitHeight = 44;
     CGFloat navbarLandscapeHeight = 32;

     CGRect initialFrame = // your initial frame
     CGFloat scaleFactor = floorf((navbarLandscapeHeight/navbarPortraitHeight) * 100) / 100;

     if (isLandscape) {
         self.frame = CGRectApplyAffineTransform(initialFrame, CGAffineTransformMakeScale(scaleFactor, scaleFactor));
     } else {
         self.frame = initialFrame;
     }
 }

Then in the InterfaceOrientation delegates I invoked the -setLandscape method on the customViews to change their sizes.

In UIViewController -

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

- (void)updateNavbarButtonsToDeviceOrientation
 {
     ResizeButton *rightButton = (ResizeButton *)self.navigationItem.rightBarButtonItem.customView;
     ResizeButton *titleView = (ResizeButton *)self.navigationItem.titleView;

     if (self.interfaceOrientation == UIDeviceOrientationPortrait || self.interfaceOrientation == UIDeviceOrientationPortraitUpsideDown) {
         [rightButton setLandscape:NO];
         [titleView setLandscape:NO];
     } else {
         [rightButton setLandscape:YES];  
         [titleView setLandscape:YES];
     }
 }
like image 20
Nate Potter Avatar answered Oct 31 '22 16:10

Nate Potter


(Answering my own question)

I got this working by manually keeping track of the titleView's margins (its distance from the edges of the navigtion bar) -- saving when the view disappears, and restoring when the view reappears.

The idea is, we aren't restoring the titleView to the exact size it had previously; rather, we are restoring it so that it has the same margins it had previously. That way, if the phone has rotated, the titleView will have a new, appropriate size.

Here is my code:

In my view controller's .h file:

@interface MyViewController ...
{
    CGRect titleSuperviewBounds;
    UIEdgeInsets titleViewMargins;
}

In my view controller's .m file:

/**
 * Helper function: Given a parent view's bounds and a child view's frame,
 * calculate the margins of the child view.
 */
- (UIEdgeInsets) calcMarginsFromParentBounds:(CGRect)parentBounds
                                  childFrame:(CGRect)childFrame {
    UIEdgeInsets margins;
    margins.left = childFrame.origin.x;
    margins.top = childFrame.origin.y;
    margins.right = parentBounds.size.width -
        (childFrame.origin.x + childFrame.size.width);
    margins.bottom = parentBounds.size.height -
        (childFrame.origin.y + childFrame.size.height);
    return margins;
}

- (void)viewDidUnload {
    [super viewDidUnload];

    titleSuperviewBounds = CGRectZero;
    titleViewMargins = UIEdgeInsetsZero;
}

- (void) viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];

    // Keep track of bounds information, so that if the user changes the
    // phone's orientation while we are in a different view, then when we
    // return to this view, we can fix the titleView's size.
    titleSuperviewBounds = self.navigationItem.titleView.superview.bounds;
    CGRect titleViewFrame = self.navigationItem.titleView.frame;
    titleViewMargins = [self calcMarginsFromParentBounds:titleSuperviewBounds
                                              childFrame:titleViewFrame];
}


- (void) viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];

    // Check for the case where the user went into a different view, then
    // changed the phone's orientation, then returned to this view.  In that
    // case, our titleView probably has the wrong size, and we need to fix it.
    if (titleSuperviewBounds.size.width > 0) {
        CGRect newSuperviewBounds =
            self.navigationItem.titleView.superview.bounds;
        if (newSuperviewBounds.size.width > 0 &&
            !CGRectEqualToRect(titleSuperviewBounds, newSuperviewBounds))
        {
            CGRect newFrame = UIEdgeInsetsInsetRect(newSuperviewBounds,
                titleViewMargins);
            newFrame.size.height =
                self.navigationItem.titleView.frame.size.height;
            newFrame.origin.y = floor((newSuperviewBounds.size.height -
                self.navigationItem.titleView.frame.size.height) / 2);
            self.navigationItem.titleView.frame = newFrame;
        }
    }
}
like image 29
Mike Morearty Avatar answered Oct 31 '22 16:10

Mike Morearty


For IOS5 onwards, as this is an old question...This is how I accomplished the same issue with the title text not aligning properly.

[[UINavigationBar appearance] setTitleVerticalPositionAdjustment:2 forBarMetrics:UIBarMetricsLandscapePhone];

Tested on ios5/6 sims works fine.

like image 1
StuartM Avatar answered Oct 31 '22 17:10

StuartM


This is what I did:

self.viewTitle.frame = self.navigationController.navigationBar.frame;
self.navigationItem.titleView = self.viewTitle;

The viewTitle is a view created in the xib, it takes the size of the navigationBar and after it has been added the titleView adjust the size to leave room to the back button. Rotations seem to work fine.

like image 1
LightMan Avatar answered Oct 31 '22 18:10

LightMan