141

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!

swiftBoy
  • 35,607
  • 26
  • 136
  • 135
Raghuram
  • 1,651
  • 2
  • 12
  • 17
  • 1
    Could you please edit your question and format your code? You can do that with cmd+k when you have your code selected. – jbehrens94 Aug 11 '16 at 10:40
  • 2
    check this out http://stackoverflow.com/questions/25666269/ios8-swift-how-to-detect-orientation-change – DSAjith Aug 11 '16 at 10:42
  • Does it print landscape/portrait correctly? – Daniel Aug 11 '16 at 10:53
  • yes it prints correctly @simpleBob – Raghuram Aug 11 '16 at 11:10
  • You should use interface orientation instead of device orientation => https://stackoverflow.com/a/60577486/8780127 – Wilfried Josset Mar 23 '20 at 13:55
  • Check this one if you want to detect changes right when the app launches. https://stackoverflow.com/questions/34452650/in-swift-how-to-get-the-device-orientation-correctly-right-after-its-launched/49058588#49058588 – eharo2 Feb 03 '21 at 18:04

15 Answers15

166
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)
        }
    }
Pablo Sanchez Gomez
  • 1,438
  • 16
  • 28
Rutvik Kanbargi
  • 3,398
  • 2
  • 16
  • 7
  • Thank you it works for imageView,what if i want to add background image to viewController?? – Raghuram Aug 11 '16 at 11:17
  • 1
    Put a imageView on background. Give constraint to the imageView for top,bottom,leading,trailing to ViewController main view to cover all the background. – Rutvik Kanbargi Aug 11 '16 at 12:46
  • 4
    Don't forget to call super.viewWillTransitionToSize within your override method – Brian Sachetta Aug 29 '17 at 02:34
  • Do you know why I can't override `viewWillTransition` when subclassing `UIView`? – Xcoder Jun 15 '18 at 00:17
  • 5
    There is another orientation type: `.isFlat`. If you only want to catch `portrait` I suggest you change the else clause to `else if UIDevice.current.orientation.isPortrait`. Note: `.isFlat` can happen in both, portrait and landscape, and with just else {} it will default there always (even if it's flat lanscape). – PMT Sep 11 '18 at 14:47
  • To handle all the possible cases you should use interface orientation instead => https://stackoverflow.com/a/60577486/8780127 – Wilfried Josset Mar 23 '20 at 13:56
  • for some reason, this did not detect portrait for me when the simulator's iPhone was upside down. – MrAn3 Apr 08 '22 at 20:28
  • Note that there is a difference between interface orientation and device orientation – Pierre Jul 27 '22 at 08:53
105

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")
    }
}
iwasrobbed
  • 46,496
  • 21
  • 150
  • 195
maslovsa
  • 1,589
  • 1
  • 14
  • 15
  • Why would you bother with using the notification center, when you can use the `viewWillTransitionToSize` delegate, and refrain from adding and removing the observers? – Michael Nov 25 '16 at 16:12
  • 1
    Definitely - my approach needs some additional code. But it really "detects orientation changes". – maslovsa Nov 28 '16 at 11:20
  • 3
    @Michael Because `viewWillTransition:` anticipates the change will happen while using `UIDeviceOrientationDidChange` is called once the change is complete. – Lyndsey Scott Jan 28 '17 at 23:50
  • 1
    @LyndseyScott I still believe that the described behaviour can be achieved without using the potentially dangerous NSNotificationCenter. Please review the following answer, and let me know your thoughts: http://stackoverflow.com/a/26944087/1306884 – Michael Feb 01 '17 at 06:55
  • 5
    What's interesting in this answer is that this will give the current orientation even when the view controller's `shouldAutorotate` is set to `false`. This is handy for screens like the Camera app's main screen--the orientation is locked but the buttons can rotate. – yoninja Mar 22 '17 at 08:28
  • This should be the accepted answer for handling orientation changes as there's no override method to determine what happens after the view rotates. For instance, I have an animation that I need to play before the view rotates and pause after it rotates to keep the frame from getting screwy if its paused when the view rotates. – froggomad Feb 20 '18 at 02:06
  • This method is also useful when you need to tell to some nested objects, not connected directly to the view controller (like a table in table cells or collection view in a table cell), that they should be updated. These objects don't have the viewWillTransitionToSize method themselves. /!\ But I noticed some really dangerous behaviour—this notification triggers by the gyroscope/accelerator even when you change the front angle of the device without rotation, what can lead to unrequested table refresh for example. Be careful to use it! – Alexey Chekanov Feb 27 '18 at 19:24
  • The answer that @Michael linked should be the accepted answer. – gokeji Feb 28 '18 at 01:43
  • 3
    As of iOS 9 you don't even need to explicitly call deinit. You can read more about it here: https://useyourloaf.com/blog/unregistering-nsnotificationcenter-observers-in-ios-9/ – James Jordan Taylor Jul 08 '18 at 13:04
  • 1
    Using viewWillTransition(to) function is not called on iPad - as its only called if a size class changes - if you need to detect orientation on an iPad, this is the correct answer. Otherwise you need to return a custom size class for the view in question. – LightningStryk Oct 14 '19 at 17:10
  • 1
    Since Swift 4.2 the notification name has been updated from `NSNotification.Name.UIDeviceOrientationDidChange` to `UIDevice.orientationDidChangeNotification` – dchakarov Nov 06 '19 at 18:55
  • Works great for ARKit rotation check as well! – The Interloper Sep 06 '20 at 23:27
  • Actually you can return the UIDevice.current.orientation.rawValue for more detail: 0:unknown, 1:portrait, 2:portraitUpsideDown, 3:landscapeLeft, 4:landscapeRight, 5:faceUp, 6:faceDown In addition to the answer: it can be checked for the booleans: isFlat, isLandscape, isPortrait, isValidInterfaceOrientation [Apple's Docu](https://developer.apple.com/documentation/uikit/uideviceorientation) – FrugalResolution Dec 22 '20 at 00:52
90

⚠️Device Orientation != Interface Orientation⚠️

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

Wilfried Josset
  • 1,096
  • 8
  • 11
  • 2
    I find this answer the best and most informative. But testing on iPadOS13.5 the use of suggested `func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator)` did not work. I've made it work using `func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator)`. – Andrej May 29 '20 at 09:39
  • Hello @WilfriedJosset, I tried this solution but it does not work when the Phone orientation is Locked. Can you please help me? – Khadija Daruwala Apr 16 '21 at 12:29
  • @PersianBlue hey! if the user lock the phone orientation, well it should not rotate/animate/trigger anything then, sounds quite logical to me, but I probably don't understand the entire scope of your question. What do you mean by "does not work"? – Wilfried Josset Apr 17 '21 at 08:51
  • @WilfriedJosset So if you check the default phone camera the icon like flash, camera switch etc rotates even when the phone rotation is locked. I was trying to implement a similar feature but I guess that would not be possible as maybe because apple has own internal way of detecting the rotation – Khadija Daruwala Apr 19 '21 at 05:52
  • @PersianBlue For this specific use case you might want to try to observe the DEVICE orientation (not interface) => https://stackoverflow.com/a/40263064/8780127 – Wilfried Josset Apr 20 '21 at 07:48
  • @WilfriedJosset I tried that already. The orientationDidChangeNotification does not get notified when the Phone orientation is locked – Khadija Daruwala Apr 20 '21 at 08:20
  • 1
    @PersianBlue well you could try to hack it by using CoreMotion => https://medium.com/@maximbilan/how-to-use-core-motion-in-ios-using-swift-1287f7422473 Might be refused by Apple though – Wilfried Josset Apr 20 '21 at 09:53
  • @WilfriedJosset Ah !! thanks a lot that sounds like a good idea – Khadija Daruwala Apr 20 '21 at 10:34
  • You saved my life! For some reason I was changing device orientation, but when I tried to get the current orientation, orientation was the same as it was before I made the rotation. – unferna May 27 '21 at 21:32
  • @WilfriedJosset love you man. save lot's of time of me – SHAH MD IMRAN HOSSAIN Mar 15 '22 at 04:43
  • windows on UIApplication is deprecated and I'm using this recently - it should work as long as your are not using several scenes (think iPad) `public extension UIApplication { static var firstSceneWindows: [UIWindow] { let windows: [UIWindow] if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene { windows = windowScene.windows } else { windows = .init() } return windows } }` and then you can replace `UIApplication.shared.windows` with `UIApplication.firstSceneWindows`. – Jonny Jun 07 '23 at 02:39
79

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")
    }
}
Josh Bernfeld
  • 4,246
  • 2
  • 32
  • 35
Yaroslav Bai
  • 1,056
  • 11
  • 14
15

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
    }
}
Josh Bernfeld
  • 4,246
  • 2
  • 32
  • 35
asetniop
  • 523
  • 1
  • 5
  • 12
  • well, I do have a similar problem in my iMessage app. Isn't it really weird? And, even your solution works the opposite!! ¯\_(ツ)_/¯ – Shyam Jun 19 '17 at 13:45
  • Do not use UIScreen.main.bounds, because it might give you the screen bounds prior to the transition (mind the 'Will' in the method), especially on multiple fast rotations. You should use the 'size' parameter... – Dan Bodnar Jul 29 '17 at 09:56
  • @dan-bodnar Do you mean the nativeBounds parameter? – asetniop Apr 04 '18 at 23:13
  • 3
    @asetniop, no. The `size` parameter that is injected in viewWillTransitionToSize:withCoordinator: method you written above. This will reflect the exact size that your view will have after the transition. – Dan Bodnar Apr 05 '18 at 07:47
  • 1
    This isn't a good solution if you are using child view controllers, the size might of the container might be square always, regardless of orientation. – bojan Mar 18 '20 at 21:22
9

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)
    }
}
Nico S.
  • 3,056
  • 1
  • 30
  • 64
8

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!

WeiseRatel
  • 545
  • 5
  • 13
  • Good point about calling super. It is really sloppy coding to not call the super method in an overridden function and it can produce unpredictable behavior in apps. And potentially bugs that are really hard to track down! – Adam Freeman Sep 30 '17 at 22:49
  • Do you know why I can't override viewWillTransition when subclassing UIView? – Xcoder Jun 15 '18 at 00:18
  • @Xcoder The reason (usually) of an object not applying an override lies in the runtime instance wasn't created with the custom class but with the default instead. If you are using Interface Builder be sure of selecting the custom class for this UIStackView object, in the Identity Inspector section at the right panel. – WeiseRatel Aug 01 '18 at 05:27
8

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)
maslovsa
  • 1,589
  • 1
  • 14
  • 15
6

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.

MH175
  • 2,234
  • 1
  • 19
  • 35
  • Overriding `willTransition(to newCollection: UITraitCollection` might also be a safe gamble. – aclima Feb 24 '22 at 14:24
5

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)
RLoniello
  • 2,309
  • 2
  • 19
  • 26
5

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.

badross92
  • 93
  • 3
  • 6
4

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

Sport
  • 8,570
  • 6
  • 46
  • 65
ingconti
  • 10,876
  • 3
  • 61
  • 48
2

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.

lenooh
  • 10,364
  • 5
  • 58
  • 49
1

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)
    //...
}
ckc
  • 345
  • 4
  • 12
1

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

ydstmw
  • 61
  • 1
  • 4