7

I'm writing a drawing app and I don't want the drawing view to rotate. At the same time, I want other controls to rotate nicely depending on the orientation of the device. In iOS 7 I've solved this via:

- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
    float rotation;

    if (toInterfaceOrientation==UIInterfaceOrientationPortrait) {
        rotation = 0;
    }
    else if (toInterfaceOrientation==UIInterfaceOrientationLandscapeLeft) {
        rotation = M_PI/2;
    } else if (toInterfaceOrientation==UIInterfaceOrientationLandscapeRight) {
        rotation = -M_PI/2;
    }

    self.drawingView.transform = CGAffineTransformMakeRotation(rotation);
    self.drawingView.frame = self.view.frame;
}

But on iOS 8 even though the function is called and the transform is set correctly, it does not prevent the view from rotating.

I've tried creating a view controller which simply prevents the rotation of it's view and add it's view to the view hierarchy, but then it doesn't respond to user input.

Any ideas?

Thanks!

Jan
  • 1,827
  • 2
  • 16
  • 29

5 Answers5

6

Okay after some fighting with subviews and transitionCoordinators I've finally figured it out:

- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
    CGAffineTransform targetRotation = [coordinator targetTransform];
    CGAffineTransform inverseRotation = CGAffineTransformInvert(targetRotation);

    [coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {

        self.drawingView.transform = CGAffineTransformConcat(self.drawingView.transform, inverseRotation);

        self.drawingView.frame = self.view.bounds;
    } completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
    }];
}

What I do is, I calculate the inverse of the transform applied to the view and then use it to change the transform. furthermore I change the frame with the view bounds. This is due to it being full screen.

Jan
  • 1,827
  • 2
  • 16
  • 29
  • I had a similar issue with keeping some camera controls straight on an iPad. This worked for me with a slight variation: I set the drawingView.frame equal to a rectangle with an origin of 0,0 and a size of the `size` that was being passed in. – Walter Sep 10 '15 at 19:32
  • When you rotate 180 deg, for example, landscape left to right, the whole view will do a "barrel roll". It's cool. It's disturbing. Up to you though ;) If you don't want this, check out this answer here: https://stackoverflow.com/a/36781371/1173143 – yuji Nov 06 '18 at 11:41
4

Dimitri's answer worked perfectly for me. This is the swift version of the code in case someone needs it...

override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
  let targetRotation = coordinator.targetTransform()
  let inverseRotation = CGAffineTransformInvert(targetRotation)

  coordinator.animateAlongsideTransition({ context in
    self.drawingView.transform = CGAffineTransformConcat(self.drawingView.transform, inverseRotation)
    self.drawingView.frame = self.view.bounds
    context.viewControllerForKey(UITransitionContextFromViewControllerKey)
  }, completion: nil)
}
JoeGalind
  • 3,545
  • 2
  • 29
  • 33
2

Swift 4 has a lot of updates, including the viewWillTransition function.

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator)

    let targetRotation = coordinator.targetTransform
    let inverseRotation = targetRotation.inverted()

    coordinator.animate(alongsideTransition: { context in
        self.drawingView.transform = self.drawingView.transform.concatenating(inverseRotation)
        self.drawingView.frame = self.view.bounds
        context.viewController(forKey: UITransitionContextViewControllerKey.from)
    }, completion: nil)
}
Chewie The Chorkie
  • 4,896
  • 9
  • 46
  • 90
1

I managed to obtain this, check:

rotation working as desired

Note: The green and the red views are subviews of the controller's view. The blue view is subview of the red view.

Idea According to https://developer.apple.com/library/archive/qa/qa1890/_index.html, we need to apply an inverse rotation to the view when it is transitioning to a new size. In addition to that, we need to adjust the constraints after rotation (landscape/portrait).

Implementation

class MyViewController: UIViewController {

    var viewThatShouldNotRotate = UIView()

    var view2 = UIView()

    var insiderView = UIView()

    var portraitConstraints: [NSLayoutConstraint]!

    var landscapeConstraints: [NSLayoutConstraint]!

    override func viewDidLoad() {
        super.viewDidLoad()

        viewThatShouldNotRotate.backgroundColor = .red

        view2.backgroundColor = .green

        insiderView.backgroundColor = .blue

        view.addSubview(viewThatShouldNotRotate)
        view.addSubview(view2)


        viewThatShouldNotRotate.addSubview(insiderView)
        portraitConstraints = createConstraintsForPortrait()
        landscapeConstraints = createConstraintsForLandscape()
    }

    func createConstraintsForLandscape() -> [NSLayoutConstraint] {
        return NSLayoutConstraint.autoCreateConstraintsWithoutInstalling {
            viewThatShouldNotRotate.autoMatch(.height, to: .width, of: view)
            viewThatShouldNotRotate.autoMatch(.width, to: .height, of: view)
            viewThatShouldNotRotate.autoCenterInSuperview()

            view2.autoPinEdgesToSuperviewEdges(with: UIEdgeInsets(), excludingEdge: .top)
            view2.autoSetDimension(.height, toSize: 100)

            insiderView.autoPinEdges(toSuperviewMarginsExcludingEdge: .bottom)
            insiderView.autoSetDimension(.height, toSize: 100)
        }
    }

    func createConstraintsForPortrait() -> [NSLayoutConstraint] {
        return NSLayoutConstraint.autoCreateConstraintsWithoutInstalling {
            viewThatShouldNotRotate.autoMatch(.height, to: .height, of: view)
            viewThatShouldNotRotate.autoMatch(.width, to: .width, of: view)
            viewThatShouldNotRotate.autoCenterInSuperview()
            view2.autoPinEdges(toSuperviewMarginsExcludingEdge: .top)
            view2.autoSetDimension(.height, toSize: 100)
            insiderView.autoPinEdges(toSuperviewMarginsExcludingEdge: .bottom)
            insiderView.autoSetDimension(.height, toSize: 100)
        }
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        if view.bounds.size.width > view.bounds.size.height {
            // landscape
            portraitConstraints.forEach {$0.autoRemove()}
            landscapeConstraints.forEach { $0.autoInstall() }
        } else {
            landscapeConstraints.forEach {$0.autoRemove()}
            portraitConstraints.forEach { $0.autoInstall() }
        }
    }

    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)

        let viewToRotate: UIView = viewThatShouldNotRotate

        coordinator.animate(alongsideTransition: { (UIViewControllerTransitionCoordinatorContext) -> Void in
            let deltaTransform = coordinator.targetTransform
            let deltaAngle = atan2f(Float(deltaTransform.b), Float(deltaTransform.a))

            var currentRotation = (viewToRotate.layer.value(forKeyPath: "transform.rotation.z") as! NSNumber).floatValue

            // Adding a small value to the rotation angle forces the animation to occur in a the desired direction, preventing an issue where the view would appear to rotate 2PI radians during a rotation from LandscapeRight -> LandscapeLeft.
            currentRotation = currentRotation + (-1 * Float(deltaAngle)) + 0.0001
            viewToRotate.layer.setValue(currentRotation, forKeyPath:"transform.rotation.z")

        }) { (UIViewControllerTransitionCoordinatorContext) -> Void in

            var currentTransform = viewToRotate.transform;
            currentTransform.a = round(currentTransform.a);
            currentTransform.b = round(currentTransform.b);
            currentTransform.c = round(currentTransform.c);
            currentTransform.d = round(currentTransform.d);
            viewToRotate.transform = currentTransform;
        }
    }
}
0

In iOS 8 transforms aren't applied to individual views owned by view controllers. Instead the rotation transforms are applied to the UIWindow. The result is that developers never see a rotation being applied, but rather a resize.

In iOS 8 you can either override the callbacks for size class changes and perform your own transform there, or you can get the orientation events as described here: https://developer.apple.com/library/prerelease/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/motion_event_basics/motion_event_basics.html.

InkGolem
  • 2,662
  • 1
  • 15
  • 22
  • I've tried rotating with the orientationDidChangeNotification and it prevents the rotation, but not the resizing and the corresponding misplacement. – Jan Feb 13 '15 at 18:48
  • @Dimitri that's correct. If you just apply the opposite transform it will be sized wrong. You need to handle size and position yourself. – InkGolem Feb 13 '15 at 21:12