Question

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.

Was it helpful?

Solution 2

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");
    }
}

OTHER TIPS

With CGAffineTransform you need to be very careful in the order you do things and (as David mentioned) you need to know what spot you are rotating or sizing about. People are often surprised by the results of the rotation, regardless of the anchorPoint (the point about which you rotate with the affine).

If we had this for our starting view:

original view

You could have the anchorPoint be at (0, 0) ,or at the center of the image. Either could work. Neither, alone, will likely give you what you want.

Here is from 0, 0:

rotated by 90

If you just wanted to rotate a view to display it, if you have the 0, 0 anchor point, you rotate and wonder "what happened to my view!!!" But then you could just translate it over to where you want.

Here is with the anchor point set in the center of the image:

rotated about center

You can see the resultant view, but its off both vertically and horizontally because its a rectangle and the sides are different lengths. You can adjust it easily, though, by moving the x by the original height difference, and moving the y by the original width difference.

From the UIView Class reference: transform - Specifies the transform applied to the receiver, relative to the center of its bounds. That means your CGAffineTransform is rotating around the center of the box, not the frame origin. You need to offset the origin relative to its displacement from the rotation. Those numbers are a function of the dimensions of the bounding box.

See here for an example of using bounds: iPhone correct landscape window coordinates

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top