Swift - How to detect orientation changes

IosSwift

Ios Problem Overview


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!

Ios Solutions


Solution 1 - Ios

let const = "Background" //image name
let const2 = "GreyBackground" // image name
    @IBOutlet weak var imageView: UIImageView!
    override func viewDidLoad() {
        super.viewDidLoad()

        imageView.image = UIImage(named: const)
        // Do any additional setup after loading the view.
    }
    
    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)
        if UIDevice.current.orientation.isLandscape {
            print("Landscape")
            imageView.image = UIImage(named: const2)
        } else {
            print("Portrait")
            imageView.image = UIImage(named: const)
        }
    }

Solution 2 - Ios

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")
    }
}

Solution 3 - Ios

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")
	}
}

Solution 4 - Ios

⚠️Device Orientation != Interface Orientation⚠️

Swift 5.* iOS15 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

Solution 5 - Ios

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
    }
}

Solution 6 - Ios

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!

Solution 7 - Ios

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)

Solution 8 - Ios

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.

Solution 9 - Ios

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)

Solution 10 - Ios

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.

Solution 11 - Ios

Here is a modern Combine solution:

import UIKit
import Combine

class MyClass: UIViewController {

     private var subscriptions = Set<AnyCancellable>()

     override func viewDidLoad() {
         super.viewDidLoad()
    
         NotificationCenter
             .default
             .publisher(for: UIDevice.orientationDidChangeNotification)
             .sink { [weak self] _ in
            
                 let orientation = UIDevice.current.orientation
                 print("Landscape: \(orientation.isLandscape)")
         }
         .store(in: &subscriptions)
    }
}

Solution 12 - Ios

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

Solution 13 - Ios

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.

Solution 14 - Ios

My app is running on iOS 15 and I have checked only on iPhone/iPad so I can't say about all use cases however I am using the following environment variable:

@Environment(\.verticalSizeClass) private var verticalSizeClass

Then checking its value using the following: verticalSizeClass == .compact is horizontal verticalSizeClass == .regular is vertical

https://developer.apple.com/documentation/swiftui/environmentvalues/verticalsizeclass

Solution 15 - Ios

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)
    //...
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionRaghuramView Question on Stackoverflow
Solution 1 - IosRutvik KanbargiView Answer on Stackoverflow
Solution 2 - IosmaslovsaView Answer on Stackoverflow
Solution 3 - IosYaroslav BaiView Answer on Stackoverflow
Solution 4 - IosWilfried JossetView Answer on Stackoverflow
Solution 5 - IosasetniopView Answer on Stackoverflow
Solution 6 - IosWeiseRatelView Answer on Stackoverflow
Solution 7 - IosmaslovsaView Answer on Stackoverflow
Solution 8 - IosMH175View Answer on Stackoverflow
Solution 9 - IosRLonielloView Answer on Stackoverflow
Solution 10 - Iosbadross92View Answer on Stackoverflow
Solution 11 - IosNico S.View Answer on Stackoverflow
Solution 12 - IosingcontiView Answer on Stackoverflow
Solution 13 - IoslenoohView Answer on Stackoverflow
Solution 14 - IosydstmwView Answer on Stackoverflow
Solution 15 - IosckcView Answer on Stackoverflow