I’m trying to update the viewWillTransition(to size: with coordinator:) method for iPhoneX.
But I can’t get the destinations of the safeAreaInsets values.
Please help!
override func viewWillTransition(to size: CGSize,
with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
if #available(iOS 11.0, *) {
if let window = UIApplication.shared.keyWindow {
let insets = window.safeAreaInsets
contentFrame = CGRect(x:insets.left, y:insets.top,
width:size.width - insets.left - insets.right,
height:size.height - insets.top - insets.bottom)
}
} else {
contentFrame = CGRect(x:0,y:0,width:size.width, height:size.height)
}
self.updateViews()
}
I got the valid solution at Stackoverflow Japan.
It's just getting the insets in the UIViewControllerTransitionCoordinator.animate(alongsideTransition:completion:) closure like below:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: { (context) in
...
let insets = ...safeAreaInsets
...
}, completion: nil)
}
I also have this issue. After experimenting and I found a nice workaround without waiting for transition to finish.
have a view (let's call it SizeListenerView) that anchor to the safe area, so that we only need to listen to this view's size change
In SizeListenerView
override var bounds: CGRect {
didSet {
// now you can access the bounds here
}
}
I have been trying to make it so that view will transition will only return something if the device is an iphone in landscape mode using the following code:
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
coordinator.animateAlongsideTransition(nil, completion: {
_ in
return x
})
}
I cant see how to make this code only execute if the device is an iphone in landscape mode or is there is a different way to do it.
Within your function, you can check the device idiom simply by using Apple's provided UIKit APIs (under UIDevice).
let isPhone: Bool = UIDevice.current.userInterfaceIdiom == .phone
Further more, using this same UIDevice API from UIKit, you can check the orientation of the device.
let isLandscape: Bool = UIDevice.current.orientation.isLandscape
For your example, you could do the following:
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
if isPhone && isLandscape {
coordinator.animateAlongsideTransition(nil, completion: { _ in
})
}
}
You can check if it is an iPhone this way:
let isPhone: Bool = UIDevice.current.userInterfaceIdiom == .phone
And you can get the screen orientation using this:
let isLandscape: Bool = UIDevice.current.orientation.isLandscape
So to do something only if it's a phone in landscape mode:
if isPhone && isLandscape {
}
I know that this question was asked before and tried to implement the suggested solution, but it doesn't work for me. Perhaps I am doing smth wrong. Do you have any idea?
Here is my code:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var test: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
test.backgroundColor = UIColor.darkGray
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated) // No need for semicolon
func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
if UIDevice.current.orientation.isLandscape {
test.backgroundColor = UIColor.purple
} else {
test.backgroundColor = UIColor.blue
}
}
}
}
You have nested functions - that won't work. Change it to this:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated) // No need for semicolon
}
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
if UIDevice.current.orientation.isLandscape {
test.backgroundColor = UIColor.purple
} else {
test.backgroundColor = UIColor.blue
}
}
In fact, you can get rid of the override for viewWillLayoutSubviews with the code you've shown.
** to those that need additional help see recommendations below
** this function will be called before the change in size and but you will know it from the arguments
Swift 3.x +
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
if UIDevice.current.orientation.isLandscape {
//Landscape
print("before transition")
print("w "+"\(width)")
print("h "+"\(height)")
print("to size")
print("w "+"\(size.width)")
print("h "+"\(size.height)")
} else {
//Portrait
print("before transition")
print("w "+"\(width)")
print("h "+"\(height)")
print("to size")
print("w "+"\(size.width)")
print("h "+"\(size.height)")
}
}
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!
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)
}
}
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")
}
}
⚠️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
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")
}
}
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 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)
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)
}
}
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.
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)
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.
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
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.
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)
//...
}
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
I'm trying to implement the new viewWillTransitionToSize method which has been introduced in iOS 8 (all other rotation methods have been deprecated). I'd like to know what the equivalent of didRotateFromInterfaceOrientation is now as there are a number of clean up tasks we need to perform and I can't see a block that we can assign to UIViewControllerTransitionCoordinator in order to be called when 'transition' to a new size finishes. Thanks.
Okay found it, just have to use the animateAlongsideTransition:completion: method on the passed UIViewControllerTransitionCoordinator.
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context)
{
UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation];
// do whatever
} completion:^(id<UIViewControllerTransitionCoordinatorContext> context)
{
}];
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
}
The Swift Version of the answer by strange
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
coordinator.animateAlongsideTransition({ (UIViewControllerTransitionCoordinatorContext) -> Void in
let orient = UIApplication.sharedApplication().statusBarOrientation
switch orient {
case .Portrait:
println("Portrait")
// Do something
default:
println("Anything But Portrait")
// Do something else
}
}, completion: { (UIViewControllerTransitionCoordinatorContext) -> Void in
println("rotation completed")
})
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
}
iOS 10.3 & Swift 3
override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
coordinator.animate(alongsideTransition: { (_) in
let orient = newCollection.verticalSizeClass
switch orient {
case .compact:
print("Lanscape")///Excluding iPads!!!
default:
print("Portrait")
}
}, completion: { _ in
print("rotation completed")
})
super.willTransition(to: newCollection, with: coordinator)
}
The accepted answer in Swift 3:
override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
coordinator.animate(alongsideTransition: { (_) in
let orient = UIApplication.shared.statusBarOrientation
switch orient {
case .portrait:
print("Portrait")
// Do something
default:
print("Anything But Portrait")
// Do something else
}
}, completion: { (UIViewControllerTransitionCoordinatorContext) -> Void in
print("rotation completed")
})
super.willTransition(to: newCollection, with: coordinator)
}
It works fine for me 👍
Since the question was: what was the equivalent of didRotateFromInterfaceOrientation
I thought I'd contribute the code below:
#implementation ViewController
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
[super traitCollectionDidChange:previousTraitCollection];
if (previousTraitCollection.verticalSizeClass == UIUserInterfaceSizeClassRegular) {
NSLog(#"User has rotated to landscape");
} else if (previousTraitCollection.verticalSizeClass == UIUserInterfaceSizeClassCompact) {
NSLog(#"User has rotated to portrait");
}
}
#end
I was testing on an iPhone in the simulator, but my print statements won't get run if I test using the iPad since the traitsCollection won't change.
This is strange because this is exactly what Apple recommends:
- (void) traitCollectionDidChange: (UITraitCollection *) previousTraitCollection {
[super traitCollectionDidChange: previousTraitCollection];
if ((self.traitCollection.verticalSizeClass != previousTraitCollection.verticalSizeClass)
|| self.traitCollection.horizontalSizeClass != previousTraitCollection.horizontalSizeClass)) {
// your custom implementation here
}
}
[[UIApplication sharedApplication] statusBarOrientation] is deprecated in iOS9 you have to test against UITraitCollection for various devices.
override func willTransitionToTraitCollection(newCollection: UITraitCollection, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
if newCollection.containsTraitsInCollection(UITraitCollection(verticalSizeClass: .Regular)) {
...
}
}
On the Ipad there is no trait collection change so here is how you detect the rotation from start and completion. Here is the Swift 5 syntax:
override func viewWillTransition(to size: CGSize, with coordinator:
UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: { [unowned self] _ in
self.view.backgroundColor = UIColor.blue
print("rotation in progress")
}) { [unowned self] _ in
self.view.backgroundColor = UIColor.green
print("rotation complete")
}
}