Child View Controller to Rotate While Parent View Controller Does Not
Asked Answered
M

3

26

What I am Trying to Do:

Parent View that is managed by Parent View Controller SHOULD NOT ROTATE.

Child View that is managed by Child View Controller SHOULD ROTATE to all orientations.


enter image description here

enter image description here


What I Have Tried:

ParentViewController

override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
    return .Portrait
}

override func shouldAutorotate() -> Bool {
    return true
}

fun addChildViewController() {
    let storyBoard = UIStoryboard(name: "Main", bundle: nil)
    self.childViewController = storyBoard("Child View Controller") as? ChildViewController
    self .addChildViewController(self.childViewController!)
    self.childViewController! .didMoveToParentViewController(self)
    self.view .addSubview(self.childViewController!.view)
    self.childViewController!.view.frame = CGRectMake (40, 200, 400, 250)
}

ChildViewController

override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
    return  .All
}

override func shouldAutorotate() -> Bool {
    return true
}

Supported orientations in Xcode Deployment Info are set to all four.

What I Get:

None of the View's rotate. If I set the parent's rotation to all, all views rotate together. So it's all or nothing.


UPDATE

When I try putting an observer for UIDeviceOrientationDidChangeNotification and use UIDevice.currentDevice().orientation to rotate childView using CGAffaineTransformMakeRotate , I get desired results. HOWEVER, i.e., if I rotate to landscape, and try to pull down the notification center (or if I get a system notification), which is still in the portrait orientation (because the app is natively still left in Portrait), rotated childView rotates back to portrait to honor the status bar/notification/notification center.

Stock iOS Camera App is the best example I can present. The main canvas does not rotate in to different orientations, yet the status bar, behind the scenes, rotates honoring device rotation. Also the subviews rotate within them selves to honor different orientations. I am trying to achieve this behavior....

Minicam answered 31/7, 2016 at 14:28 Comment(7)
Not sure if this would work, take it just as an idea. I would definitely set the app to support only one (portrait) orientation, and then I would try to detect the rotation of the device (not the screen) and respond to that by manually rotating the view in the middle of the screen. I guess viewWillTransition(to:with:) would not work, so you'll need to find an appropriate solution.Treacle
You mean rotate using something like CGAffineTransform?Minicam
Yes, exactly CGAffineTransform. You have an interesting situation. Would love to help and actually try to write some code to address this issue, but I'm afraid I'll be quiet busy, so I won't be able to get my hands dirty here.Treacle
Well, I was leaving CGAffineTransform rotate method as a plan Z. I am surprised how above code doesn't work. It just doesn't pass 'shouldAutorotate()' up the view controllers.Minicam
Updated the OP mentioning the problem with that route...Minicam
Does the rotating ViewController have to be a child of the non-rotating ViewController? If it wasn't a child, you could just have two separate view controllers, similar to this answerBernadettebernadina
Note that, due to changes in the underlying implementation of autorotation in iOS 8, adding subviews to the application's window may no longer be an effective technique: developer.apple.com/library/ios/qa/qa1890/_index.htmlMarcelinomarcell
M
2

After many back and forth's with Apple them selves, and them pointing to the same link Technical Q&A QA1890, I had to do this with the flowing way:

MotherViewController

override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {

        super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)

        coordinator.animateAlongsideTransition({(context: UIViewControllerTransitionCoordinatorContext) -> Void in
            let deltaTransform: CGAffineTransform = coordinator.targetTransform()
            let deltaAngle: CGFloat = atan2(deltaTransform.b, deltaTransform.a)
            var currentRotation: CGFloat = self.mainView.layer.valueForKeyPath("transform.rotation.z") as! CGFloat

            // 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 += -1 * deltaAngle + 0.0001
            self.mainView.layer.setValue(currentRotation, forKeyPath: "transform.rotation.z")

            }, completion: {(
                context: UIViewControllerTransitionCoordinatorContext) -> Void in
                // Integralize the transform to undo the extra 0.0001 added to the rotation angle.
                var currentTransform: CGAffineTransform = self.mainView.transform
                currentTransform.a = round(currentTransform.a)
                currentTransform.b = round(currentTransform.b)
                currentTransform.c = round(currentTransform.c)
                currentTransform.d = round(currentTransform.d)
                self.mainView.transform = currentTransform
        })
    }

MainViewController

NSNotificationCenter.defaultCenter().addObserver(self, selector: "orientationChanged:", name:UIDeviceOrientationDidChangeNotification, object: nil)

func orientationChanged ( notification: NSNotification){
        switch UIDevice.currentDevice().orientation {
        case .Portrait:
            self.rotateChildViewsForOrientation(0)
        case .PortraitUpsideDown:
            self.rotateChildViewsForOrientation(0)
        case .LandscapeLeft:
            self.rotateChildViewsForOrientation(90)
        case .LandscapeRight:
            self.rotateChildViewsForOrientation(-90)
        default:
            print ("Default")
                }
    }

This seem to rotate the child view's while keeping the main view static. Also the app seem to know the proper orientation when notifications come in (because mother view actually rotates behind the scenes).

Special thanks to jamesk and Warif Akhand Rishi for all the help!

Minicam answered 16/8, 2016 at 15:56 Comment(2)
This doesn't seem to work for me, as described at #42778855 Any idea why?Aluminum
can you elaborate on this answer? what is rotateChildViewsForOrientation? There's no declaration of the functionPolivy
M
13

The effect achieved in the Camera app is similar to using a combination of:

The technical note explains that the underlying implementation of autorotation involves applying a transform directly to the application's window:

Autorotation is implemented by applying a rotation transform to the application's window when the system determines that the interface must rotate. The window then adjusts its bounds for the new orientation and propagates this change down through the view controller hierarchy via calls to each view controller's and presentation controller's viewWillTransitionToSize:withTransitionCoordinator: method.

Note that the Camera app does not opt out of autorotation: when using the Camera app, the orientations of Notification Center and Control Center reflect the device orientation. It is only particular views within the Camera app that appear to opt out of autorotation. In fact, this answer suggests that such apps typically utilise the videoGravity and videoOrientation properties provided by AVFoundation classes such as AVCaptureVideoPreviewLayer and AVCaptureConnection.

The approach you should take depends on whether you only want to stop a parent view from rotating or whether you want to stop a container view controller such as UINavigationController from rotating (i.e. lock device orientation).

If the former, use the technique described in the technical note to fix the orientation of the parent view, and (for iOS 9) wrap rotating subviews in a UIStackView and use the axis property to rearrange the subviews on device rotation, or (for iOS 8) manually rotate the subviews on device rotation (as to which see this answer and this sample code).

If the latter, the approach you're taking is on the right track. For sample code, see this answer. You'll need to share your code and more information about your view controller hierarchy for us to diagnose quirks involving Notification Center.

Addendum: The reason why calls to shouldAutorotate and supportedInterfaceOrientations were not being propagated to child view controllers was explained in Technical Q&A QA1688:

Beginning with iOS 6, only the topmost view controller (alongside the UIApplication object) participates in deciding whether to rotate in response to a change of the device's orientation. More often than not, the view controller displaying your content is a child of UINavigationController, UITabBarController, or a custom container view controller. You may find yourself needing to subclass a container view controller in order to modify the values returned by its supportedInterfaceOrientations and shouldAutorotate methods.

In Swift, you can override those methods by using extensions.

Marcelinomarcell answered 10/8, 2016 at 13:30 Comment(8)
Great reply! I am still reading the links. As I mentioned below, I am having issues using viewWillTransitionToSize:withTransitionCoordinator: because I want the motherView to NOT rotate while childView to rotate. If motherView does not rotate at all, viewWillTransitionToSize:withTransitionCoordinator: would not be called within it self.Minicam
Here's the trick: don't return false from shouldAutorotate. Use the technique shown in the technical note to prevent motherView from rotating—which won't interfere with viewWillTransitionToSize:—and use the latter (or viewWillLayoutSubviews) to rotate your subviews accordingly.Marcelinomarcell
Let us help! What's misbehaving?Marcelinomarcell
Well, your links were extremely helpful. I know this is achievable using stackView's & Autolayout; However, viewWillTransitionToSize: method seems keep things cleaner for this particular project.Minicam
Going back to the beginning: I want to keep the mainView static while the childView rotates. After all that, when a notification comes in, the status bar has to be in the proper orientation. Referring to iOS Camera App, Main interface stays still, flash icon rotates. I need the exact behavior. If were to use viewWillTransitionToSize: method, to how can I use it inside mainViewController, if thats the view I am trying to stop from rotating? Also, I DO want the childView to rotate.Minicam
So far, I came to a complicated method using viewWillTransitionToSize: and UIDevice.currentDevice().orientation with UIDeviceOrientationDidChangeNotification KVO. Use viewWillTransitionToSize: to stop rotating the mainView. use UIDevice.currentDevice().orientation with UIDeviceOrientationDidChangeNotification KVO to rotate the childView using CGAffineTransformMakeRotation.Minicam
Is the issue with that method the same as what you described in the OP—about the child view resetting its rotation when you access Notification Center? Are you overriding shouldAutorotate with this solution?Marcelinomarcell
This doesn't seem to work for me, as described at #42778855 Any idea why?Aluminum
A
3

From iOS developer library Technical Q&A

Autorotation is implemented by applying a rotation transform to the application's window when the system determines that the interface must rotate. The window then adjusts its bounds for the new orientation and propagates this change down through the view controller hierarchy via calls to each view controller's and presentation controller's -viewWillTransitionToSize:withTransitionCoordinator: method. Invocations of this method are provided a transition coordinator object containing the delta of the window's current transform and new transform, which can be retrieved by sending a -targetTransform message to the transition coordinator. Your view controller or presentation controller can derive the appropriate inverse transform and apply it to the target view. This nullifies the effect of the window transform on that particular view and gives the appearance that the view has remained fixed in its current orientation as the rest of the interface rotates normally.

i.e. if you have a view named noRotatingView

override func viewWillLayoutSubviews() {
    super.viewWillLayoutSubviews()

    noRotatingView.center = CGPointMake(CGRectGetMidX(self.view.bounds), CGRectGetMidY(self.view.bounds))
}

override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)

    coordinator.animateAlongsideTransition({ (UIViewControllerTransitionCoordinatorContext) -> Void in

        let deltaTransform = coordinator.targetTransform()
        let deltaAngle = atan2f(Float(deltaTransform.b), Float(deltaTransform.a))

        var currentRotation = self.noRotatingView.layer.valueForKeyPath("transform.rotation.z")?.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
        self.noRotatingView.layer.setValue(currentRotation, forKeyPath:"transform.rotation.z")

        }) { (UIViewControllerTransitionCoordinatorContext) -> Void in

            var currentTransform = self.noRotatingView.transform;
            currentTransform.a = round(currentTransform.a);
            currentTransform.b = round(currentTransform.b);
            currentTransform.c = round(currentTransform.c);
            currentTransform.d = round(currentTransform.d);
            self.noRotatingView.transform = currentTransform;
    }
}

I've tried to make a demo project at https://github.com/rishi420/TestCameraRotation

Ambulator answered 10/8, 2016 at 13:38 Comment(4)
Great job with your answer! Looking at your sample code, I keep coming to the main issue I am still having even when I was looking at Apple's Technical documentation for this: MotherView does not rotate, ChildView rotates. viewWillTransitionToSize method would not be called if the containing view does not rotate at all. This is where I am still stuck...Minicam
@gizmodo I think mother view rotates in iOS camera app. coz notification center and control center rotates. If you want to lock the mother view you can see RotateButton.Ambulator
Combination of your TestCameraRotation + RotateButton makes the best sense at this point. 1. Put MainView inside another view and use viewWillTransitionToSize to stop it from rotating. 1. Use device orientation KVO to rotate childView.Minicam
This doesn't seem to work for me, as described at #42778855 Any idea why? @MinicamAluminum
M
2

After many back and forth's with Apple them selves, and them pointing to the same link Technical Q&A QA1890, I had to do this with the flowing way:

MotherViewController

override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {

        super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)

        coordinator.animateAlongsideTransition({(context: UIViewControllerTransitionCoordinatorContext) -> Void in
            let deltaTransform: CGAffineTransform = coordinator.targetTransform()
            let deltaAngle: CGFloat = atan2(deltaTransform.b, deltaTransform.a)
            var currentRotation: CGFloat = self.mainView.layer.valueForKeyPath("transform.rotation.z") as! CGFloat

            // 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 += -1 * deltaAngle + 0.0001
            self.mainView.layer.setValue(currentRotation, forKeyPath: "transform.rotation.z")

            }, completion: {(
                context: UIViewControllerTransitionCoordinatorContext) -> Void in
                // Integralize the transform to undo the extra 0.0001 added to the rotation angle.
                var currentTransform: CGAffineTransform = self.mainView.transform
                currentTransform.a = round(currentTransform.a)
                currentTransform.b = round(currentTransform.b)
                currentTransform.c = round(currentTransform.c)
                currentTransform.d = round(currentTransform.d)
                self.mainView.transform = currentTransform
        })
    }

MainViewController

NSNotificationCenter.defaultCenter().addObserver(self, selector: "orientationChanged:", name:UIDeviceOrientationDidChangeNotification, object: nil)

func orientationChanged ( notification: NSNotification){
        switch UIDevice.currentDevice().orientation {
        case .Portrait:
            self.rotateChildViewsForOrientation(0)
        case .PortraitUpsideDown:
            self.rotateChildViewsForOrientation(0)
        case .LandscapeLeft:
            self.rotateChildViewsForOrientation(90)
        case .LandscapeRight:
            self.rotateChildViewsForOrientation(-90)
        default:
            print ("Default")
                }
    }

This seem to rotate the child view's while keeping the main view static. Also the app seem to know the proper orientation when notifications come in (because mother view actually rotates behind the scenes).

Special thanks to jamesk and Warif Akhand Rishi for all the help!

Minicam answered 16/8, 2016 at 15:56 Comment(2)
This doesn't seem to work for me, as described at #42778855 Any idea why?Aluminum
can you elaborate on this answer? what is rotateChildViewsForOrientation? There's no declaration of the functionPolivy

© 2022 - 2024 — McMap. All rights reserved.