[ios] Swift - How to detect orientation changes

I want to add two images to single image view (i.e for landscape one image and for portrait another image)but i don't know how to detect orientation changes using swift languages.

I tried this answer but it takes only one image

override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
    if UIDevice.currentDevice().orientation.isLandscape.boolValue {
        print("Landscape")
    } else {
        print("Portrait")
    }
}

I am new to iOS development,any advice would be greatly appreciated!

This question is related to ios swift

The answer is


Swift 4+: I was using this for soft keyboard design, and for some reason the UIDevice.current.orientation.isLandscape method kept telling me it was Portrait, so here's what I used instead:

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

    if(size.width > self.view.frame.size.width){
        //Landscape
    }
    else{
        //Portrait
    }
}

If your are using Swift version >= 3.0 there are some code updates you have to apply as others have already said. Just don't forget to call super:

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

   super.viewWillTransition(to: size, with: coordinator)

   // YOUR CODE OR FUNCTIONS CALL HERE

}

If you are thinking to use a StackView for your images be aware you can do something like the following:

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

   super.viewWillTransition(to: size, with: coordinator)

   if UIDevice.current.orientation.isLandscape {

      stackView.axis = .horizontal

   } else {

      stackView.axis = .vertical

   } // else

}

If your are using Interface Builder don't forget to select the custom class for this UIStackView object, in the Identity Inspector section at the right panel. Then just create (also through Interface Builder) the IBOutlet reference to the custom UIStackView instance:

@IBOutlet weak var stackView: MyStackView!

Take the idea and adapt it to your needs. Hope this can help you!


Swift 3 Above code updated:

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

    if UIDevice.current.orientation.isLandscape {
        print("Landscape")
    } else {
        print("Portrait")
    }
}

I believe the correct answer is actually a combination of both approaches: viewWIllTransition(toSize:) and NotificationCenter's UIDeviceOrientationDidChange.

viewWillTransition(toSize:) notifies you before the transition.

NotificationCenter UIDeviceOrientationDidChange notifies you after.

You have to be very careful. For example, in UISplitViewController when the device rotates into certain orientations, the DetailViewController gets popped off the UISplitViewController's viewcontrollers array, and pushed onto the master's UINavigationController. If you go searching for the detail view controller before the rotation has finished, it may not exist and crash.


To get the correct orientation on app start you have to check it in viewDidLayoutSubviews(). Other methods described here won't work.

Here's an example how to do it:

var mFirstStart = true

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    if (mFirstStart) {
        mFirstStart = false
        detectOrientation()
    }
}

func detectOrientation() {
    if UIDevice.current.orientation.isLandscape {
        print("Landscape")
        // do your stuff here for landscape
    } else {
        print("Portrait")
        // do your stuff here for portrait
    }
}

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    detectOrientation()
}

This will work always, on app first start, and if rotating while the app is running.


Swift 4.2, RxSwift

If we need to reload collectionView.

NotificationCenter.default.rx.notification(UIDevice.orientationDidChangeNotification)
    .observeOn(MainScheduler.instance)
    .map { _ in }            
    .bind(to: collectionView.rx.reloadData)
    .disposed(by: bag)

Swift 4, RxSwift

If we need to reload collectionView.

NotificationCenter.default.rx.notification(NSNotification.Name.UIDeviceOrientationDidChange)
    .observeOn(MainScheduler.instance)
    .map { _ in }            
    .bind(to: collectionView.rx.reloadData)
    .disposed(by: bag)

You can use viewWillTransition(to:with:) and tap into animate(alongsideTransition:completion:) to get the interface orientation AFTER the transition is complete. You just have to define and implement a protocol similar to this in order to tap into the event. Note that this code was used for a SpriteKit game and your specific implementation may differ.

protocol CanReceiveTransitionEvents {
    func viewWillTransition(to size: CGSize)
    func interfaceOrientationChanged(to orientation: UIInterfaceOrientation)
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)

        guard
            let skView = self.view as? SKView,
            let canReceiveRotationEvents = skView.scene as? CanReceiveTransitionEvents else { return }

        coordinator.animate(alongsideTransition: nil) { _ in
            if let interfaceOrientation = UIApplication.shared.windows.first?.windowScene?.interfaceOrientation {
                canReceiveRotationEvents.interfaceOrientationChanged(to: interfaceOrientation)
            }
        }

        canReceiveRotationEvents.viewWillTransition(to: size)
    }

You can set breakpoints in these functions and observe that interfaceOrientationChanged(to orientation: UIInterfaceOrientation) is always called after viewWillTransition(to size: CGSize) with the updated orientation.


Using NotificationCenter and UIDevice's beginGeneratingDeviceOrientationNotifications

Swift 4.2+

override func viewDidLoad() {
    super.viewDidLoad()        

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

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

func rotated() {
    if UIDevice.current.orientation.isLandscape {
        print("Landscape")
    } else {
        print("Portrait")
    }
}

Swift 3

override func viewDidLoad() {
    super.viewDidLoad()        

    NotificationCenter.default.addObserver(self, selector: #selector(ViewController.rotated), name: NSNotification.Name.UIDeviceOrientationDidChange, object: nil)
}

deinit {
     NotificationCenter.default.removeObserver(self)
}

func rotated() {
    if UIDevice.current.orientation.isLandscape {
        print("Landscape")
    } else {
        print("Portrait")
    }
}

Another way to detect device orientations is with the function traitCollectionDidChange(_:). The system calls this method when the iOS interface environment changes.

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?)
{
    super.traitCollectionDidChange(previousTraitCollection)
    //...
}

Furthermore, you can use function willTransition(to:with:) ( which is called before traitCollectionDidChange(_:) ), to get information just before the orientation is applied.

 override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator)
{
    super.willTransition(to: newCollection, with: coordinator)
    //...
}

??Device Orientation != Interface Orientation??

Swift 5.* iOS14 and below

You should really make a difference between:

  • Device Orientation => Indicates the orientation of the physical device
  • Interface Orientation => Indicates the orientation of the interface displayed on the screen

There are many scenarios where those 2 values are mismatching such as:

  • When you lock your screen orientation
  • When you have your device flat

In most cases you would want to use the interface orientation and you can get it via the window:

private var windowInterfaceOrientation: UIInterfaceOrientation? {
    return UIApplication.shared.windows.first?.windowScene?.interfaceOrientation
}

In case you also want to support < iOS 13 (such as iOS 12) you would do the following:

private var windowInterfaceOrientation: UIInterfaceOrientation? {
    if #available(iOS 13.0, *) {
        return UIApplication.shared.windows.first?.windowScene?.interfaceOrientation
    } else {
        return UIApplication.shared.statusBarOrientation
    }
}

Now you need to define where to react to the window interface orientation change. There are multiple ways to do that but the optimal solution is to do it within willTransition(to newCollection: UITraitCollection.

This inherited UIViewController method which can be overridden will be trigger every time the interface orientation will be change. Consequently you can do all your modifications in the latter.

Here is a solution example:

class ViewController: UIViewController {
    override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
        super.willTransition(to: newCollection, with: coordinator)
        
        coordinator.animate(alongsideTransition: { (context) in
            guard let windowInterfaceOrientation = self.windowInterfaceOrientation else { return }
            
            if windowInterfaceOrientation.isLandscape {
                // activate landscape changes
            } else {
                // activate portrait changes
            }
        })
    }
    
    private var windowInterfaceOrientation: UIInterfaceOrientation? {
        return UIApplication.shared.windows.first?.windowScene?.interfaceOrientation
    }
}

By implementing this method you'll then be able to react to any change of orientation to your interface. But keep in mind that it won't be triggered at the opening of the app so you will also have to manually update your interface in viewWillAppear().

I've created a sample project which underlines the difference between device orientation and interface orientation. Additionally it will help you to understand the different behavior depending on which lifecycle step you decide to update your UI.

Feel free to clone and run the following repository: https://github.com/wjosset/ReactToOrientation


Swift 4

I've had some minor issues when updating the ViewControllers view using UIDevice.current.orientation, such as updating constraints of tableview cells during rotation or animation of subviews.

Instead of the above methods I am currently comparing the transition size to the view controllers view size. This seems like the proper way to go since one has access to both at this point in code:

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator)
    print("Will Transition to size \(size) from super view size \(self.view.frame.size)")

    if (size.width > self.view.frame.size.width) {
        print("Landscape")
    } else {
        print("Portrait")
    }

    if (size.width != self.view.frame.size.width) {
        // Reload TableView to update cell's constraints.
    // Ensuring no dequeued cells have old constraints.
        DispatchQueue.main.async {
            self.tableView.reloadData()
        }
    }


}

Output on a iPhone 6:

Will Transition to size (667.0, 375.0) from super view size (375.0, 667.0) 
Will Transition to size (375.0, 667.0) from super view size (667.0, 375.0)

All previous contributes are fine, but a little note:

a) if orientation is set in plist, only portrait or example, You will be not notified via viewWillTransition

b) if we anyway need to know if user has rotated device, (for example a game or similar..) we can only use:

NotificationCenter.default.addObserver(self, selector: #selector(ViewController.rotated), name: NSNotification.Name.UIDeviceOrientationDidChange, object: nil)

tested on Xcode8, iOS11