I have a UIToolbar
in my iOS app that I rotate programmatically in the willRotateToInterfaceOrientation
function using a CGAffineTransform
. The transform requires scaling, rotation, and translation. I know how to calculate the scale and rotation parameters but cannot seem to figure out how to calculate the translation parameters so that the view will be in the correct location. After lots of trial and error, I determined the exact numbers to move the view to the proper location on the iPad, but I'd prefer not to hard-code the numbers so that the code is not too device-dependent. How do I calculate the translation parameters?
Here is my current willRotateToInterfaceOrientation
function in the view controller that contains the toolbar. I would like to know how to calculate tx
and ty
below rather than hard-coding the values. I have tried using various functions of the toolbar and window size, but the toolbar always appears overlapping the window in a strange location or outside the window completely rather than at the bottom of the window.
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
if (UIInterfaceOrientationIsLandscape(toInterfaceOrientation))
{
// First, apply identify transformation to simplify later calculations and also because we need the toolbar's frame and Apple's docs say you cannot use a view's frame if the transform property is not the identity matrix.
self.toolbar.transform = CGAffineTransformIdentity;
// Calculate affine parameters.
// Expand width to the window's height when in landscape mode, leaving the toolbar's height unchanged.
UIWindow *window = [[UIApplication sharedApplication] keyWindow];
CGFloat sx = 1.0;
CGFloat sy = window.frame.size.height / window.frame.size.width;
// Rotate 90 degrees in either direction depending on the orientation direction change.
CGFloat rotate = M_PI_2;
if (toInterfaceOrientation == UIInterfaceOrientationLandscapeLeft)
{
rotate = -M_PI_2;
}
// Reposition the toolbar.
// TODO: Calculate values rather than hard-coding them. Why is tx not something like ((window.frame.size.width / 2) - (self.toolbar.frame.size.height / 2))?
// Note that these values work for both the original iPad and iPad Retina, presumably because the values represent points not pixels.
CGFloat tx = -365.5;
CGFloat ty = 359.5;
if (toInterfaceOrientation == UIInterfaceOrientationLandscapeLeft)
{
tx = -tx;
}
// Apply affine transformation.
CGAffineTransform transform = CGAffineTransformMakeScale(sx, sy);
transform = CGAffineTransformRotate(transform, rotate);
transform = CGAffineTransformTranslate(transform, tx, ty);
[UIView animateWithDuration:duration
animations:^{
self.toolbar.transform = transform;
}
completion:NULL
];
}
else if (toInterfaceOrientation == UIInterfaceOrientationPortrait)
{
[UIView animateWithDuration:duration
animations:^{
self.toolbar.transform = CGAffineTransformIdentity;
}completion:NULL
];
}
else if (toInterfaceOrientation == UIInterfaceOrientationPortraitUpsideDown)
{
NSLog(@"Upside-down orientation not supported");
}
}
Additionally, here are the declaration and basic initialization of my toolbar. Note that I am adding the toolbar as a subview to the main window and manually handling rotation because my app has a UITabBarController
as the root view controller and this was the only way I could get the toolbar to appear on top of the tab bar.
// The toolbar is strong since this controller must maintain ownership as the toolbar is removed from the parent view when this view disappears.
@property (strong, nonatomic) IBOutlet UIToolbar *toolbar;
UIWindow *window = [[UIApplication sharedApplication] keyWindow];
CGFloat height = CGRectGetHeight(self.tabBarController.tabBar.frame); // Completely cover the tab bar.
CGFloat x = window.frame.origin.x;
CGFloat y = window.frame.size.height - height;
CGFloat width = window.frame.size.width;
self.toolbar.frame = CGRectMake(x, y, width, height);
[window addSubview:self.toolbar];
Finally, I am using Xcode 4.5.1 with ARC enabled and am testing on both the iPad (Retina and non-Retina) 5.1 and 6.0 simulators.
I was able to solve this problem after setting the anchor point for my toolbar from the default of (.5, .5) to (0, 0).
// The toolbar is strong since this controller must maintain ownership as the toolbar is removed from the parent view when this view disappears.
@property (strong, nonatomic) IBOutlet UIToolbar *toolbar;
UIWindow *window = [[UIApplication sharedApplication] keyWindow];
// Set anchor point to the top-left corner instead of to the center (.5, .5) so that affine transformations during orientation changes are easier to calculate.
self.toolbar.layer.anchorPoint = CGPointMake(0, 0);
CGFloat height = CGRectGetHeight(self.tabBarController.tabBar.frame); // Completely cover the tab bar.
CGFloat x = window.frame.origin.x;
CGFloat y = window.frame.size.height - height;
CGFloat width = window.frame.size.width;
self.toolbar.frame = CGRectMake(x, y, width, height);
[window addSubview:self.toolbar];
Here's the corrected code where the translation parameters tx
and ty
are calculated as a function of the toolbar and window sizes rather than being hard-coded. I wrapped the logic in the new willRotateView:toInterfaceOrientation:duration:
function to make it easier to reuse and rotate an arbitrary view.
willRotateToInterfaceOrientation:duration:
just forwards the rotation request on to this function with my toolbar as the view parameter.
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
[self willRotateView:self.toolbar toInterfaceOrientation:toInterfaceOrientation duration:duration];
}
- (void)willRotateView:(UIView *)view toInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
if (UIInterfaceOrientationIsLandscape(toInterfaceOrientation))
{
// First, apply identify transformation to simplify later calculations and also because we need the view's frame and Apple's docs say you cannot use a view's frame if the transform property is not the identity matrix.
view.transform = CGAffineTransformIdentity;
// Calculate affine parameters.
// Get the window's post-orientation change dimensions.
UIWindow *window = [[UIApplication sharedApplication] keyWindow];
CGFloat rotatedWinWidth = window.frame.size.height;
CGFloat rotatedWinHeight = window.frame.size.width;
// Expand width to the window's height when in landscape mode, leaving the view's height unchanged. The scaling is done along the Y axis since the window's origin will change such that the Y axis will still run along the longer direction of the device.
CGFloat sx = 1.0;
CGFloat sy = rotatedWinWidth / rotatedWinHeight;
// Rotate 90 degrees in either direction depending on the orientation direction change.
CGFloat angle = M_PI_2;
if (toInterfaceOrientation == UIInterfaceOrientationLandscapeLeft)
{
angle = -M_PI_2;
}
// Reposition the view, assuming that view.layer.anchorPoint is (0, 0).
// Note that the height of the view is used as the X offset as this corresponds to the X direction since the rotated window's origin will also rotate. Also, the position has to take into account the width scale factor.
CGFloat xOffset = view.frame.size.height;
CGFloat tx = -(rotatedWinWidth - xOffset) / sy;
CGFloat ty = -view.frame.size.height;
if (toInterfaceOrientation == UIInterfaceOrientationLandscapeLeft)
{
tx = -xOffset / sy;
ty = rotatedWinHeight - view.frame.size.height;
}
// Apply affine transformation.
CGAffineTransform transform = CGAffineTransformMakeScale(sx, sy);
transform = CGAffineTransformRotate(transform, angle);
transform = CGAffineTransformTranslate(transform, tx, ty);
[UIView animateWithDuration:duration
animations:^{
view.transform = transform;
}
completion:NULL
];
}
else if (toInterfaceOrientation == UIInterfaceOrientationPortrait)
{
[UIView animateWithDuration:duration
animations:^{
view.transform = CGAffineTransformIdentity;
}completion:NULL
];
}
else if (toInterfaceOrientation == UIInterfaceOrientationPortraitUpsideDown)
{
DLog(@"Upside-down orientation not supported");
}
}