I want to change the layout and constraints relative to the orientation of the device.
Here's how I implemented it:
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
addViews()
constraintSubviews()
}
func constraintSubviews() {
if UIDevice.current.orientation.isPortrait {
NSLayoutConstraint.activate(portraitConstrains)
NSLayoutConstraint.deactivate(landscapeConstrains)
NSLayoutConstraint.activate(nowPlayingView.portraitConstrains)
NSLayoutConstraint.deactivate(nowPlayingView.landscapeConstrains)
} else {
NSLayoutConstraint.deactivate(portraitConstrains)
NSLayoutConstraint.activate(landscapeConstrains)
NSLayoutConstraint.activate(nowPlayingView.landscapeConstrains)
NSLayoutConstraint.deactivate(nowPlayingView.portraitConstrains)
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
constraintSubviews()
}
Here's what I ended up when running the code:
And this is what I expect:
I can't get the layout I designed in the code unless I change device orientation once.
Better implementation
I've searched all over the Internet and the best thing I found was this implementation. Is there a better implementation regarding this subject?
You need
1- Make deactivate before activate
func constraintSubviews() {
if UIDevice.current.orientation.isPortrait {
NSLayoutConstraint.deactivate(landscapeConstrains)
NSLayoutConstraint.deactivate(nowPlayingView.landscapeConstrains)
NSLayoutConstraint.activate(nowPlayingView.portraitConstrains)
NSLayoutConstraint.activate(portraitConstrains)
} else {
NSLayoutConstraint.deactivate(portraitConstrains)
NSLayoutConstraint.deactivate(nowPlayingView.portraitConstrains)
NSLayoutConstraint.activate(landscapeConstrains)
NSLayoutConstraint.activate(nowPlayingView.landscapeConstrains)
}
}
2- Replace
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
constraintSubviews()
}
with
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.willTransition(to: size, with: coordinator)
constraintSubviews()
}
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
}
}
func SetDeviceFontSizes()
{
...
imgView.frame.size.width = 375
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
SetDeviceFontSizes()
}
override func viewWillLayoutSubviews() {
SetDeviceFontSizes()
}
override func viewDidLoad() {
super.viewDidLoad()
SetDeviceFontSizes()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
SetDeviceFontSizes()
}
I want to set sizes for labels and images (for old devices these sizes will be small).
But when I change orientation of device, the sizes return to default values in spite of function SetDeviceFontSizes().
How can I set frame sizes after change orientation?
var m_CurrentOrientation = UIDeviceOrientation()
add this line in your viewdidload()
UIDevice.currentDevice().beginGeneratingDeviceOrientationNotifications()
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(self.deviceOrientationDidChange), name: .DidChangeNotification, object: nil)
perform your task in if condition
func OrientationDidChange(notification: NSNotification) {
var Orientation = UIDevice.currentDevice().orientation
if Orientation == .LandscapeLeft || Orientation == .LandscapeRight {
}
else if Orientation == .Portrait {
}
}
i'm not sure this will solve your problem, but you should call super in viewWillLayoutSubviews and viewWillTransition
You can use UITraitCollection. Refer to this link
How to translate this objective c traitCollection to swift?
Do not call anything in viewWillLayoutSubviews() function.
Hope it helps you.
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
In my UINavigationController, I add an UIView by code.
I want to remove this UIView when my View disappear but I can't find the right way to do that. Here's the I'm using:
var patch: UIView!
override func viewDidLoad() {
super.viewDidLoad()
patch = UIView(frame: CGRectMake(0, 0, view.bounds.width, 20))
patch.backgroundColor = UIColor.redColor()
self.view.addSubview(patch)
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(true)
patch.removeFromSuperview()
// or
self.view.willRemoveSubview(patch)
}
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
if UIDevice.currentDevice().orientation.isLandscape.boolValue {
patch.hidden = true
} else {
patch.hidden = false
}
}
and nothing, view is still there.
I tried even setting patch to nil or to CGRectMake(0, 0, 0, 0) but nothing...
What's the right code to delete it?