What is the "right" way to handle orientation changes in iOS 8?
Asked Answered
H

5

107

Can someone please tell me the "right" or "best" approach to working with portrait and landscape interface orientations in iOS 8? It seems that all the functions I want to use for that purpose are deprecated in iOS 8, and my research has turned up no clear, elegant alternative. Am I really supposed to look at the width and height to determine for myself if we are in portrait or landscape mode?

For example, in my view controller, how should I implement the following pseudocode?

if we are rotating from portrait to landscape then
  do portrait things
else if we are rotating from landscape to portrait then
  do landscape things
Hydrology answered 26/9, 2014 at 23:53 Comment(2)
Read the docs for UIViewController. See the section titled "Handling View Rotations`. It explains what you should do.Justus
That they are deprecated is a clue. You gotta use something else....that something else should be AutoLayout and Size Classes :-)Detention
I
268

Apple recommends using size classes as a coarse measure of how much screen space is available, so that your UI can significantly change its layout and appearance. Consider that an iPad in portrait has the same size classes as it does in landscape (Regular width, Regular height). This means that your UI should be more or less similar between the two orientations.

However, the change from portrait to landscape in an iPad is significant enough that you may need to make some smaller adjustments to the UI, even though the size classes have not changed. Since the interface orientation related methods on UIViewController have been deprecated, Apple now recommends implementing the following new method in UIViewController as a replacement:

- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator
{
    [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];

    // Code here will execute before the rotation begins.
    // Equivalent to placing it in the deprecated method -[willRotateToInterfaceOrientation:duration:]

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

        // Place code here to perform animations during the rotation.
        // You can pass nil or leave this block empty if not necessary.

    } completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {

        // Code here will execute after the rotation has finished.
        // Equivalent to placing it in the deprecated method -[didRotateFromInterfaceOrientation:]

    }];
}

Great! Now you're getting callbacks right before the rotation starts, and after it finishes. But what about actually knowing whether the rotation is to portrait or to landscape?

Apple recommends thinking about rotation as simply a change in size of the parent view. In other words, during an iPad rotation from portrait to landscape, you can think of it as the root-level view simply changing its bounds.size from {768, 1024} to {1024, 768}. Knowing this then, you should use the size passed into the viewWillTransitionToSize:withTransitionCoordinator: method above to figure out whether you are rotating to portrait or landscape.

If you want an even more seamless way to migrate legacy code to the new iOS 8 way of doing things, consider using this simple category on UIView, which can be used to determine whether a view is "portrait" or "landscape" based on its size.

To recap:

  1. You should use size classes to determine when to show fundamentally different UIs (e.g. an "iPhone-like" UI vs. an "iPad-like" UI)
  2. If you need to make smaller adjustments to your UI when size classes don't change but your container (parent view) size does, such as when an iPad rotates, use the viewWillTransitionToSize:withTransitionCoordinator: callback in UIViewController.
  3. Every view in your app should only make layout decisions based on the space that it has been given to layout in. Let the natural hierarchy of views cascade this information down.
  4. Similarly, don't use the statusBarOrientation -- which is basically a device-level property -- to determine whether to layout a view for "portrait" vs "landscape". The status bar orientation should only be used by code dealing with things like UIWindow which actually live at the very root level of the app.
Impatient answered 10/12, 2014 at 19:50 Comment(5)
Excellent answer! BTW, your youtube video on AL was incredibly informative. Thanx for sharing. Check it out! youtube.com/watch?v=taWaW2GzfCIStone
The only problem I found is that sometimes you want to know device orientation, and this won't let you know it. On iPad it's called before status bar or device orientation value changes. You can't tell if user holds device in portrait or portrait-upside-down. It's also impossible to test, as mocking UIViewControllerTransitionCoordinator is a nightmare :-(Fabricant
@Fabricant have you found an elegant solution to cope with those issues?Esther
so where is viewdidlayoutsubviews then? I thought viewdidlayoutsubviews is used to capture bound changes due to rotation. Can you elaborate about that?Sanjak
How to differentiate between landscape left vs landscape right if we do not use statusbarorientation? I need that distinction. Besides, there are other problems as described here - #53364998Accoucheur
L
18

Based on smileyborg's very well detailed (and accepted) answer, here is an adaptation using swift 3:

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator)
    coordinator.animate(alongsideTransition: nil, completion: {
        _ in
        self.collectionView.collectionViewLayout.invalidateLayout()
    })        
}

And in the UICollectionViewDelegateFlowLayout implementation,

public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    // retrieve the updated bounds
    let itemWidth = collectionView.bounds.width
    let itemHeight = collectionView.bounds.height
    // do whatever you need to do to adapt to the new size
}
Laudian answered 2/10, 2016 at 20:0 Comment(0)
K
11

I simply use notification Center:

Add an orientation variable (will explain at end)

//Above viewdidload
var orientations:UIInterfaceOrientation = UIApplication.sharedApplication().statusBarOrientation

Add Notification when view appears

override func viewDidAppear(animated: Bool) {
    NSNotificationCenter.defaultCenter().addObserver(self, selector: "orientationChanged:", name: UIDeviceOrientationDidChangeNotification, object: nil)
}

Remove Notification when view goes away

override func viewWillDisappear(animated: Bool) {
        NSNotificationCenter.defaultCenter().removeObserver(self, name: UIDeviceOrientationDidChangeNotification, object: nil) 
}

Gets current orientation when notification is triggered

func orientationChanged (notification: NSNotification) {
    adjustViewsForOrientation(UIApplication.sharedApplication().statusBarOrientation)
}

Checks orientation (portrait/landscape) and handles events

func adjustViewsForOrientation(orientation: UIInterfaceOrientation) {
    if (orientation == UIInterfaceOrientation.Portrait || orientation == UIInterfaceOrientation.PortraitUpsideDown)
    {
        if(orientation != orientations) {
            println("Portrait")
            //Do Rotation stuff here
            orientations = orientation
        }
    }
    else if (orientation == UIInterfaceOrientation.LandscapeLeft || orientation == UIInterfaceOrientation.LandscapeRight)
    {
       if(orientation != orientations) {
            println("Landscape")
            //Do Rotation stuff here
            orientations = orientation
        }
    }
}

The reason I add an orientation variable is because when testing on a physical device the orientation notification gets called at every minor move in the device, and not just when it rotates. Adding the var and if statements only calls the code if it switched to the opposite orientation.

Kinney answered 10/12, 2014 at 20:10 Comment(4)
This isn't Apple's recommended approach because it means every view in your app is deciding how to display based on the device's orientation, instead of considering the space it's been given.Impatient
Apple also isn't taking hardware into consideration with 8.0. Tell an AVPreview layer it doesn't need to worry about orientation if it's presented over a view controller. Doesn't work, but it's fixed in 8.2Calabar
super of viewDidAppear and viewWillDisappear should be calledEtter
Note : UIDeviceOrientation != UIInterfaceOrientation. In my experiments, statusBarOrientation is not reliable.Sememe
D
2

From a UI perspective, I believe that using Size Classes are Apple's recommended approach for handling interfaces in different orientations, sizes and scales.

See the section: Traits Describe the Size Class and Scale of an Interface here: https://developer.apple.com/library/ios/releasenotes/General/WhatsNewIniOS/Articles/iOS8.html

"iOS 8 adds new features that make dealing with screen size and orientation much more versatile."

This one is a good article as well: https://carpeaqua.com/thinking-in-terms-of-ios-8-size-classes/

EDIT Updated Link: https://carpeaqua.com/2014/06/14/thinking-in-terms-of-ios-8-size-classes/ (Credit: Koen)

Detention answered 27/9, 2014 at 0:1 Comment(5)
Yeah, looks like his whole site is down at the moment.Detention
The article is indeed worth reading and here is the correct address: carpeaqua.com/thinking-in-terms-of-ios-8-size-classesFulfil
But what if we are not talking about size classes and UI, but about AVFoundation, for example. When recording video, in some cases you must know the viewControllers orientation to set correct metadata. This is simple impossible. "versatile".Jato
That's not what the OP was asking/talking about.Detention
Updated link: carpeaqua.com/2014/06/14/…Unmannerly
L
0

Here is a workaround:

    private var _viewSize: CGSize = .zero
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
    
        if _viewSize.equalTo(.zero) {
            _viewSize = self.view.frame.size
        } else if _viewSize.equalTo(self.view.frame.size) == false {
            onViewSizeChanged(from: _viewSize, to: self.view.frame.size)
            _viewSize = self.view.frame.size
        }
    }

    func onViewSizeChanged(from: CGSize, to: CGSize) {
        // self.view changed size after rotation
        // your code... 
    }
Largehearted answered 24/1, 2023 at 16:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.