How to detect orientation change?
Asked Answered
N

25

173

I am using Swift and I want to be able to load a UIViewController when I rotate to landscape, can anyone point me in the right direction?

I Can't find anything online and a little bit confused by the documentation.

Nagoya answered 4/9, 2014 at 13:3 Comment(4)
I suppose that the API has not changed so it should be "didRotateToOrientation" and "willRotateToOrientation", something like that, take a look in the Apple documentationSoren
Hi @mArm.ch, thanks for the quick reply! So how would I implement this? (This is my first app... I'm very new to IOS) :)Nagoya
I reposted as answer, for other people. Can you accept it if it's ok for you ?Soren
Check this one if you want to check orientation when the app launched. #34453150Fayalite
N
221

Here's how I got it working:

In AppDelegate.swift inside the didFinishLaunchingWithOptions function I put:

NotificationCenter.default.addObserver(self, selector: #selector(AppDelegate.rotated), name: UIDevice.orientationDidChangeNotification, object: nil)

and then inside the AppDelegate class I put the following function:

func rotated() {
    if UIDeviceOrientationIsLandscape(UIDevice.current.orientation) {
        print("Landscape")
    }

    if UIDeviceOrientationIsPortrait(UIDevice.current.orientation) {
        print("Portrait")
    }
}
Nagoya answered 4/9, 2014 at 13:59 Comment(17)
I tried adding the addObserver in the AppDelegate but kept getting a SIGABRT in CoreFoundation with an unrecognised selector. However, when I moved the addObserver to a viewDidLoad in my first view it worked perfectly. Just for information if anyone comes across the same issue.Limoges
Bare in mind that you have to call: UIDevice.currentDevice().beginGeneratingDeviceOrientationNotifications() otherwise you wont get any notification.Eal
Im new to coding but shouldn't selector have a string format of "rotated:"??Corot
Im pretty sure thats only if you are accepting arguments (which rotated() doesn't)Nagoya
Awesome. I know this is a bit old, but it did help me when overriding the viewDidLayoutSubviews(){} method in my view controller. Definitely worth the upvote thanks!Cubiculum
Selector always should accept one argument, as per doc: Selector that specifies the message the receiver sends notificationObserver to notify it of the notification posting. The method specified by notificationSelector must have one and only one argument (an instance of NSNotification).Pol
Be careful because UIDeviceOrientation is something different to UIInterfaceOrientation.. This is because UIDeviceOrientation also detects face down and face up orientations meaning that your code can jump between portrait and landscape randomly if your device is resting on an almost flat but slightly uneven surface (i.e rocking on the protruding camera of the 6/6s)Coadjutant
This method DO NOT WORK if the phone disabled auto rotation.Servais
@Servais the question is about auto rotation thoughHesiod
To expand on the comment from @Coadjutant - if you tilt the device slightly forward an backward (without changing the portrait/landscape orientation), the UIDeviceOrientationDidChange notification is sent. Although the screen orientation is unchanged, the device orientation has changed in 3D space, therefore you get the notificationBeckibeckie
for swift 4.2 you will need to do something like this: NotificationCenter.default.addObserver(self, selector: #selector(AppDelegate.rotated), name: UIDevice.orientationDidChangeNotification, object: nil)Dorsy
How to override AppDelegate.rotated parameter to get rid of UIViewController ??Eastwards
it returns false when in flat position but view is landscapeJacobinism
Is there a way to get UIInterfaceOrientation through a notification?Hazen
This solution is very good. Guys that are saying to put alo of code in AppDelegate I advice to create a singleton and use inside of this two methods.Weig
Here notification name is update: Use -NSNotification.Name.UIDeviceOrientationDidChangeNook
In my case, I am seeing this notification being called also in the case whenever a native popup is dismissed. So I think we should avoid using itGranjon
L
192

According to the Apple docs:

This method is called when the view controller's view's size is changed by its parent (i.e. for the root view controller when its window rotates or is resized).

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator)
    
    if UIDevice.current.orientation.isLandscape {
        print("Landscape")
    }
    if UIDevice.current.orientation.isFlat {
        print("Flat")
    } else {
        print("Portrait")
    }
}
Leodora answered 11/1, 2015 at 17:19 Comment(6)
This will be called BEFORE the rotation. If you need e.g. the frame size after the rotation this solution will not work.Mountaintop
didn't work in extension. landscape or portrait still print "portraight"Aunt
This method DO NOT WORK if the phone disabled auto rotation.Servais
on iPad, since size class is the same regular for both landscape and portrait, this method is never called.Tolmach
This will also fail if the device returns isFlat. developer.apple.com/documentation/uikit/uideviceorientation/…Unmannered
It's painful to see all those people forgetting to call super everytime... People needs to understand what override means.Dysart
U
67

I need to detect rotation while using the camera with AVFoundation, and found that the didRotate (now deprecated) & willTransition methods were unreliable for my needs. Using the notification posted by David did work, but is not current for Swift 3.x & above.

The following makes use of a closure, which appears to be Apple's preference going forward.

var didRotate: (Notification) -> Void = { notification in
        switch UIDevice.current.orientation {
        case .landscapeLeft, .landscapeRight:
            print("landscape")
        case .portrait, .portraitUpsideDown:
            print("Portrait")
        default:
            print("other (such as face up & down)")
        }
    }

To set up the notification:

NotificationCenter.default.addObserver(forName: UIDevice.orientationDidChangeNotification,
                                       object: nil,
                                       queue: .main,
                                       using: didRotate)

To tear down the notification:

NotificationCenter.default.removeObserver(self,
                                          name: UIDevice.orientationDidChangeNotification,
                                          object: nil)

Regarding the deprecation statement, my initial comment was misleading, so I wanted to update that. As noted, the usage of @objc inference has been deprecated, which in turn was needed to use a #selector. By using a closure instead, this can be avoided and you now have a solution that should avoid a crash due to calling an invalid selector.

Unmannered answered 19/10, 2016 at 19:51 Comment(7)
Do you have reference to where Apple talking about discouraging the use of #selector in Swift 4? I would like to read up on why they are saying this.Spiccato
@Spiccato Certainly, I do not have a direct link to an Apple document, but I did include a screenshot of the compiler warning that XCode provides if you use the previous approach.Unmannered
I have added a link to swift-evolution that discusses the change.Unmannered
@CodeBender: The compiler warning does not mean what you suggest. #selector is not being depreciated, only "@objc" inference. This means that when using a function as a #selector you have to explicitly mark it, so that the compiler will generate the additional correct code because the compiler will no longer try infer it from your usage. Hence, if you add "@obj" to your rotate() func in your Swift 3.0 solution, the code will compile without the warning.Romaic
Thanks @Mythlandia, I updated the answer to resolve the confusion over my initial statement.Unmannered
how to access class variable or function inside didRotate var? any ideaSubmarginal
How do I use this in something like: if portrait { runFunctionA } else if landscape { runFunctionB } else {runFunctionC } ?Sternutatory
S
21

Using -orientation property of UIDevice is not correct (even if it could work in most of cases) and could lead to some bugs, for instance UIDeviceOrientation consider also the orientation of the device if it is face up or down, there is no direct pair in UIInterfaceOrientation enum for those values.
Furthermore, if you lock your app in some particular orientation, UIDevice will give you the device orientation without taking that into account.
On the other side iOS8 has deprecated the interfaceOrientation property on UIViewController class.
There are 2 options available to detect the interface orientation:

  • Use the status bar orientation
  • Use size classes, on iPhone if they are not overridden they could give you a way to understand the current interface orientation

What is still missing is a way to understand the direction of a change of interface orientation, that is very important during animations.
In the session of WWDC 2014 "View controller advancement in iOS8" the speaker provides a solution to that problem too, using the method that replaces -will/DidRotateToInterfaceOrientation.

Here the proposed solution partially implemented, more info here:

func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
        let orientation = orientationFromTransform(coordinator.targetTransform())
        let oldOrientation = UIApplication.sharedApplication().statusBarOrientation
        myWillRotateToInterfaceOrientation(orientation,duration: duration)
        coordinator.animateAlongsideTransition({ (ctx) in
            self.myWillAnimateRotationToInterfaceOrientation(orientation,
            duration:duration)
            }) { (ctx) in
                self.myDidAnimateFromInterfaceOrientation(oldOrientation)
        }
    }
Spurious answered 19/4, 2016 at 18:16 Comment(0)
P
13

Since iOS 8 this is the correct way to do it.

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

    coordinator.animate(alongsideTransition: { context in
        // This is called during the animation
    }, completion: { context in
        // This is called after the rotation is finished. Equal to deprecated `didRotate`
    })
}
Purdy answered 12/2, 2018 at 8:35 Comment(1)
Much better answer than NotificationCenter.default.addObserver(self, selector: #selector(AppDelegate.rotated), name: UIDevice.orientationDidChangeNotification, object: nil). The issue with UIDeviceOrientation is that it detects face down and face up orientations which we don't need.Salvation
F
12

I know this question is for Swift, but since it's one of the top links for a Google search and if you're looking for the same code in Objective-C:

// add the observer
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(rotated:) name:UIDeviceOrientationDidChangeNotification object:nil];

// remove the observer
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIDeviceOrientationDidChangeNotification object:nil];

// method signature
- (void)rotated:(NSNotification *)notification {
    // do stuff here
}
Fogle answered 6/6, 2015 at 23:23 Comment(0)
P
11

Easy, this works in iOS8 and 9 / Swift 2 / Xcode7, just put this code inside your viewcontroller.swift. It will print the screen dimensions with every orientation change, you can put your own code instead:

override func didRotateFromInterfaceOrientation(fromInterfaceOrientation: UIInterfaceOrientation) {
        getScreenSize()
    }
    var screenWidth:CGFloat=0
    var screenHeight:CGFloat=0
    func getScreenSize(){
        screenWidth=UIScreen.mainScreen().bounds.width
        screenHeight=UIScreen.mainScreen().bounds.height
        print("SCREEN RESOLUTION: "+screenWidth.description+" x "+screenHeight.description)
    }
Petigny answered 29/9, 2015 at 10:30 Comment(3)
This function was deprecated, use "viewWillTransitionToSize:withTransitionCoordinator: " insteadDiscontinuity
Beside being deprecated, didRotateFromInterfaceOrientation() doesn't work reliably. It misses some rotations. iewWillTransitionToSize:withTransitionCoordinator: works ok.Assegai
@Assegai Lots of things are now deprecated thanks to Swift 3Petigny
J
11

I like checking the orientation notification because you can add this feature in any class, no needs to be a view or a view controller. Even in your app delegate.

SWIFT 5:

    //ask the system to start notifying when interface change
    UIDevice.current.beginGeneratingDeviceOrientationNotifications()
    //add the observer
    NotificationCenter.default.addObserver(
        self,
        selector: #selector(orientationChanged(notification:)),
        name: UIDevice.orientationDidChangeNotification,
        object: nil)

than caching the notification

    @objc func orientationChanged(notification : NSNotification) {
        //your code there
    }
Jinn answered 8/8, 2016 at 9:39 Comment(0)
D
10

Use the new viewWillTransitionToSize(_:withTransitionCoordinator:)

Dreddy answered 5/9, 2014 at 1:22 Comment(0)
J
8

In Objective C

-(void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator

In swift

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

Override this method to detect the orientation change.

Johnsonjohnsonese answered 9/4, 2016 at 3:33 Comment(0)
B
8

Swift 3 | UIDeviceOrientationDidChange Notification Observed Too Often

The following code prints "deviceDidRotate" every time your device changes orientation in 3D space - regardless of a change from portrait to landscape orientation. For example, if you hold your phone in portrait orientation and tilt it forward and backward - deviceDidRotate() is called repeatedly.

override func viewDidLoad() {
    super.viewDidLoad()
    NotificationCenter.default.addObserver(
        self, 
        selector:  #selector(deviceDidRotate), 
        name: .UIDeviceOrientationDidChange, 
        object: nil
    )
}

func deviceDidRotate() {
    print("deviceDidRotate")
}

To work around this you could hold the previous device orientation and check for a change in deviceDidRotate().

var previousDeviceOrientation: UIDeviceOrientation = UIDevice.current.orientation

override func viewDidLoad() {
    super.viewDidLoad()
    NotificationCenter.default.addObserver(
        self, 
        selector:  #selector(deviceDidRotate), 
        name: .UIDeviceOrientationDidChange, 
        object: nil
    )
}

func deviceDidRotate() {
    if UIDevice.current.orientation == previousDeviceOrientation { return }
    previousDeviceOrientation = UIDevice.current.orientation
    print("deviceDidRotate")
}

Or you can use a different notification that only gets called when the device changes from landscape to portrait. In this case you'd want to use the UIApplicationDidChangeStatusBarOrientation notification.

override func viewDidLoad() {
    super.viewDidLoad()
    NotificationCenter.default.addObserver(
        self, 
        selector:  #selector(deviceDidRotate), 
        name: .UIApplicationDidChangeStatusBarOrientation, 
        object: nil
    )
}

func deviceDidRotate() {
    print("deviceDidRotate")
}
Beckibeckie answered 17/3, 2017 at 17:7 Comment(0)
B
6
override func didRotate(from fromInterfaceOrientation: UIInterfaceOrientation) {
    //swift 3
    getScreenSize()
}


func getScreenSize(){
   let screenWidth = UIScreen.main.bounds.width
   let  screenHeight = UIScreen.main.bounds.height
    print("SCREEN RESOLUTION: \(screenWidth.description) x \(screenHeight.description)")
}
Barlow answered 15/9, 2016 at 7:35 Comment(2)
Cleanest answer. Worked for me.Particulate
This is now deprecatedChibouk
S
6

Full working implementation of how to detect orientation change in Swift 3.0.

I chose to use this implementation because phone orientations of face up and face down were important to me, and I wanted the view to change only once I knew the orientation was in the specified position.

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        //1
        NotificationCenter.default.addObserver(self, selector: #selector(deviceOrientationDidChange), name: NSNotification.Name.UIDeviceOrientationDidChange, object: nil)

    }

    deinit {
        //3
        NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIDeviceOrientationDidChange, object: nil)
    }

    func deviceOrientationDidChange() {
        //2
        switch UIDevice.current.orientation {
        case .faceDown:
            print("Face down")
        case .faceUp:
            print("Face up")
        case .unknown:
            print("Unknown")
        case .landscapeLeft:
            print("Landscape left")
        case .landscapeRight:
            print("Landscape right")
        case .portrait:
            print("Portrait")
        case .portraitUpsideDown:
            print("Portrait upside down")
        }
    }

}

The important pieces to note are:

  1. You listen to the DeviceOrientationDidChange notification stream and tie it to the function deviceOrientationDidChange
  2. You then switch on the device orientation, be sure to take notice that there is an unknown orientation at times.
  3. Like any notification, before the viewController is deinitialized, make sure to stop observing the notification stream.

Hope someone finds this helpful.

Striped answered 6/2, 2017 at 2:24 Comment(3)
Thank you , greatConvulsant
So I'm confused, currently all my views adjust based on portrait to landscape, but it doesnt change if the device is face-up? How would I use your above code to achieve the same effect when its face-up!?Oligopsony
Can you give a little more context? I'm not sure what effect you're referring to. This code works to simply detect orientation. If you're using multiple methods for detecting orientation you might run into trouble.Striped
C
6

If you want to do something AFTER the rotation is complete, you can use the UIViewControllerTransitionCoordinator completion handler like this

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

    // Hook in to the rotation animation completion handler
    coordinator.animate(alongsideTransition: nil) { (_) in
        // Updates to your UI...
        self.tableView.reloadData()
    }
}
Capper answered 24/11, 2017 at 19:8 Comment(0)
B
6

Swift 4:

override func viewWillAppear(_ animated: Bool) {
    NotificationCenter.default.addObserver(self, selector: #selector(deviceRotated), name: UIDevice.orientationDidChangeNotification, object: nil)
}

override func viewWillDisappear(_ animated: Bool) {
    NotificationCenter.default.removeObserver(self, name: UIDevice.orientationDidChangeNotification, object: nil)
}

@objc func deviceRotated(){
    if UIDevice.current.orientation.isLandscape {
        //Code here
    } else {
        //Code here
    }
}

A lot of answers dont help when needing to detect across various view controllers. This one does the trick.

Balfour answered 24/1, 2019 at 4:6 Comment(3)
You also need to check if device was rotated while the view controller was disappeared (in case of other view controller is opened above current). In fact, you can skip viewWillDisappear and add addObserver in viewDidLoad. iOS unsubscribes view controller automatically.Lycanthrope
you forgot UIDevice.current.orientation.isFlatJacobinism
@Balfour this does not work when the Phone Orientation is Locked. Any idea how that can be fixed?Allegorical
A
4

Check if rotation had changed with: viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator)

With the coordinator.animateAlongsideTransition(nil) { (UIViewControllerTransitionCoordinatorContext) you can check if the transition is finished.

See code below:

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

    super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)

    coordinator.animateAlongsideTransition(nil) { (UIViewControllerTransitionCoordinatorContext) in
        // if you want to execute code after transition finished
        print("Transition finished")
    }

    if size.height < size.width {
        // Landscape
        print("Landscape")
    } else {
        // Portrait
        print("Portrait")
    }

}
Amorist answered 3/4, 2017 at 14:4 Comment(0)
T
4

Here is an easy way to detect the device orientation: (Swift 3)

override func willRotate(to toInterfaceOrientation: UIInterfaceOrientation, duration: TimeInterval) {
            handleViewRotaion(orientation: toInterfaceOrientation)
        }

    //MARK: - Rotation controls
    func handleViewRotaion(orientation:UIInterfaceOrientation) -> Void {
        switch orientation {
        case .portrait :
            print("portrait view")
            break
        case .portraitUpsideDown :
            print("portraitUpsideDown view")
            break
        case .landscapeLeft :
            print("landscapeLeft view")
            break
        case .landscapeRight :
            print("landscapeRight view")
            break
        case .unknown :
            break
        }
    }
Tarpley answered 12/5, 2017 at 6:25 Comment(1)
willRotate is deprecated now, better use viewWillTransition.Burgin
H
4

Swift 5

Setup class to receive notifications of device orientation change:

class MyClass {

    ...

    init (...) {

        ...

        super.init(...)

        // subscribe to device orientation change notifications
        UIDevice.current.beginGeneratingDeviceOrientationNotifications()
        NotificationCenter.default.addObserver(self, selector: #selector(orientationChanged), name: UIDevice.orientationDidChangeNotification, object: nil)

        ...

    }

    ...

}

Setup handler code:

@objc extension MyClass {
    func orientationChanged(_ notification: NSNotification) {
        let device = notification.object as! UIDevice
        let deviceOrientation = device.orientation

        switch deviceOrientation {
        case .landscapeLeft:   //do something for landscape left
        case .landscapeRight:  //do something for landscape right
        case .portrait:        //do something for portrait
        case .portraitUpsideDown: //do something for portrait upside-down 
        case .faceDown:        //do something for face down
        case .faceUp:          //do something for face up
        case .unknown:         //handle unknown
        @unknown default:      //handle unknown default
        }
    }
}
Heligoland answered 11/10, 2019 at 13:14 Comment(0)
S
3

My approach is similar to what bpedit shows above, but with an iOS 9+ focus. I wanted to change the scope of the FSCalendar when the view rotates.

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

    coordinator.animateAlongsideTransition({ (context) in
        if size.height < size.width {
            self.calendar.setScope(.Week, animated: true)
            self.calendar.appearance.cellShape = .Rectangle
        }
        else {
            self.calendar.appearance.cellShape = .Circle
            self.calendar.setScope(.Month, animated: true)

        }

        }, completion: nil)
}

This below worked, but I felt sheepish about it :)

coordinator.animateAlongsideTransition({ (context) in
        if size.height < size.width {
            self.calendar.scope = .Week
            self.calendar.appearance.cellShape = .Rectangle
        }
        }) { (context) in
            if size.height > size.width {
                self.calendar.scope = .Month
                self.calendar.appearance.cellShape = .Circle
            }
    }
Scotticism answered 7/4, 2016 at 23:35 Comment(0)
T
2

I use UIUserInterfaceSizeClass to detect a orientation changed in a UIViewController class just like that:

override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {

    let isiPadLandscapePortrait = newCollection.horizontalSizeClass == .regular && newCollection.verticalSizeClass == .regular
    let isiPhonePlustLandscape = newCollection.horizontalSizeClass == .regular && newCollection.verticalSizeClass == .compact
    let isiPhonePortrait = newCollection.horizontalSizeClass == .compact && newCollection.verticalSizeClass == .regular
    let isiPhoneLandscape = newCollection.horizontalSizeClass == .compact && newCollection.verticalSizeClass == .compact

     if isiPhonePortrait {
         // do something...
     }
}
Trinette answered 1/3, 2017 at 1:7 Comment(0)
C
1

For Swift 3

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    if UIDevice.current.orientation.isLandscape {
        //Landscape
    }
    else if UIDevice.current.orientation.isFlat {
        //isFlat
    }
    else {
        //Portrait
    }
}
Colvert answered 5/2, 2017 at 7:27 Comment(1)
you forgot UIDevice.current.orientation.isFlatJacobinism
N
1

With iOS 13.1.2, orientation always return 0 until device is rotated. I need to call UIDevice.current.beginGeneratingDeviceOrientationNotifications() before any rotation event occurs to get actual rotation.

Ningpo answered 17/10, 2019 at 16:37 Comment(0)
W
0
- (void)viewDidLoad {
  [super viewDidLoad];
  [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(OrientationDidChange:) name:UIDeviceOrientationDidChangeNotification object:nil];
}

-(void)OrientationDidChange:(NSNotification*)notification {
  UIDeviceOrientation Orientation=[[UIDevice currentDevice]orientation];

  if(Orientation==UIDeviceOrientationLandscapeLeft || Orientation==UIDeviceOrientationLandscapeRight) {
    NSLog(@"Landscape");
  } else if(Orientation==UIDeviceOrientationPortrait) {
    NSLog(@"Potrait Mode");
  }
}

NOTE: Just use this code to identify UIViewController is in which orientation

Washerwoman answered 13/6, 2017 at 10:32 Comment(1)
Your code is not working in project. do I need to do anything else as well?Sloop
T
0
override func viewDidLoad() {
    NotificationCenter.default.addObserver(self, selector: #selector(MyController.rotated), name: UIDevice.orientationDidChangeNotification, object: nil)
//...
}

@objc
private func rotated() {
    if UIDevice.current.orientation.isLandscape {

    } else if UIDevice.current.orientation.isPortrait {

    }

    //or you can check orientation separately UIDevice.current.orientation
    //portrait, portraitUpsideDown, landscapeLeft, landscapeRight... 

}
Toolis answered 4/10, 2019 at 11:16 Comment(0)
A
-1

Iphone 15 pro traits on landscape orientation is compact and compact. Iphone 15 pro max is regular in horizantal trait on lanscape mode. It is so strange from Apple!

Af answered 28/2 at 16:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.