I'm getting some strange results when querying UIScreen.main.bounds.height after an orientation change. Maybe this isn't the correct way to do it.
I have an observer that listens for the NSNotification.Name.UIDeviceOrientationDidChange event:
NotificationCenter.default.addObserver(self, selector: #selector(self.orientationChange), name: NSNotification.Name.UIDeviceOrientationDidChange, object: nil)
This calls a function that sets a constraint to 75% of the new screen height. This works fine on iPhone but iPad returns the wrong screen height.
If the iPad is in landscape orientation UIScreen.main.bounds.height will return a value equal to the height in portrait orientation and vice versa.
func orientationChange() {
// This will print the correct value on iPhone and iPad.
if UIDevice.current.orientation.isLandscape {
print("Landscape")
} else {
print("Portrait")
}
let screenSize = UIScreen.main.bounds
let screenHeight = screenSize.height
self.searchViewHeightConstraint.constant = screenHeight * 0.75
// Correct value on iPhone. Incorrect on iPad.
print("screenHeight: '\(screenHeight)'")
UIView.animate(withDuration: 0.6, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0, options: .curveEaseOut, animations: {
self.searchView.superview?.layoutIfNeeded()
}, completion: nil)
}
I've also come across the viewWilltransition method of monitoring orientation change but this behaves in the exact opposite way to the method above. ie. the height is correct on iPad but incorrect on iPhone:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
let screenSize = UIScreen.main.bounds
let screenHeight = screenSize.height
self.searchViewHeightConstraint.constant = screenHeight * 0.75
// Correct value on iPad. Incorrect on iPhone.
print("screenHeight: '\(screenHeight)'")
}
What is the reason for this inconsistent behaviour between iPhone and iPad and is using NotificationCenter the correct approach to monitoring orientation change?
What you should be using is
viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator). This will give you the size and is WAY more reliable than using a notification.
Also, if you wanted to do animations, inside of the UIViewControllerTransitionCoordinator you can leverage the method animate(alongsideTransition animation: ((UIViewControllerTransitionCoordinatorContext) -> Swift.Void)?, completion: ((UIViewControllerTransitionCoordinatorContext) -> Swift.Void)? = nil) -> Bool
You need to use :
dispatch_async(dispatch_get_main_queue()) {
println("h:\(view.bounds.size.height)")
println("w:\(view.frame.size.width)")
}
// All you need to write code under dispatch_async
This code will be executed after the rotation was completed (in the main queue) and the new sizes are available.
This works for me in Swift 2.3 for both iPad and iPhone.
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
print(size.height)
if UIDevice.currentDevice().orientation.isLandscape {
print("Landscape")
print(size.height)
}
else {
print("Portrait")
print(size.height)
}
}
Related
I'm not using storyboard for my app. All design I've done programatically.
Now I'm badly stuck in screen resizing. See my example code for navigation bar:-
let screenSize: CGRect = UIScreen.main.bounds
screenWidth = screenSize.width
screenHeight = screenSize.height
NameHeight = screenHeight * 0.09
NameWidth = screenWidth
navBar = UINavigationBar(frame: CGRect(x: 0, y: 30, width: NameWidth, height: NameHeight))
self.view.addSubview(navBar)
navBar.setItems([navItem], animated: false)
This navigation bar is perfectly working for both landscape and portrait.
Problem is happening:-
1) if I suddenly move my iPad from landscape to portrait or vice versa
then screen is not properly fit.
2) but if I click any button or anything then screen is properly fit.
That means when screen is refreshing then view is good.
My total app code done in this manner.
Are there any solution for my issue?
Does I need to add anything in AppDelegate which will help to reload the screnn??? or any idea please....
Help me please to overcome this issue.....
I'd have to see the code to know what the exact issue is, but you can try:
override func didRotateFromInterfaceOrientation(fromInterfaceOrientation: UIInterfaceOrientation) {
// Reload Data here
self.view.setNeedsDisplay()
The below piece of code saved me:-
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
if UIDevice.current.orientation.isLandscape {
self.viewDidLoad()
}else if UIDevice.current.orientation.isFlat {
self.viewDidLoad()
} else {
self.viewDidLoad()
}
}
May be I did the wrong thing. But it saved me ...
I've implemented the iOS 11 feature prefersLargeTitles and it works just fine. Portrait mode is working as expected:
I understand the large title will always stay collapsed (small) in landscape mode and that's fine to me. The problem is when I try to change to landscape and then again to portrait, the large title should be expanded (big) by default back in portrait mode, but it won't until I scroll down a bit:
My code looks quite simple:
if #available(iOS 11.0, *) {
navigationController?.navigationBar.prefersLargeTitles = true
navigationItem.largeTitleDisplayMode = .always
}
I also tried using different values on tableView.contentInsetAdjustmentBehavior, nothing changed. I'm kind of solving it by now scrolling down the table programmatically after orientation changes, but I think that's just a (not very nice) workaround.
Is that supposed to be working as expected? Is it something left in my implementation? Is there a better workaround to this?
I faced the same issue. This worked for me.
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
navigationItem.largeTitleDisplayMode = .always
coordinator.animate(alongsideTransition: { (_) in
self.coordinator?.navigationController.navigationBar.sizeToFit()
}, completion: nil)
}
One approach could be save the maximum navigation bar height, and set it during rotation.
Something like this:
var maximumHeight: CGFloat = 0
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
guard let navigationController = navigationController else {
return
}
if maximumHeight < navigationController.navigationBar.frame.height {
maximumHeight = navigationController.navigationBar.frame.height
}
coordinator.animate(alongsideTransition: { (_) in
navigationController.navigationBar.frame.size.height = self.maximumHeight
}, completion: nil)
}
In landscape, the system knows that it must change its size, so you don't have to worry about it.
#rassar #twofish
iOS 16
First set the PrefersLargeTitles to true either on ViewDidLoad() or on the NavigationBar in the storyboard.
Then on the Navigation Controller's rootController add this:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate { (_) in
self.navigationController?.navigationBar.sizeToFit()
}
}
I have to check if my device has changed orientation in iOS 8+.
My approach is:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
let isLand = UIScreen.main.bounds.width > UIScreen.main.bounds.height
coordinator.animate(alongsideTransition: nil) { _ in
let isLand2 = UIScreen.main.bounds.width > UIScreen.main.bounds.height
print("\(isLand) -> \(isLand2)")
}
}
it works fine in iPhone but in iPad isLand has already the new value which should be after the orientation completion, so:
Portrait > Landscape: true -> true
Landscape > Portrait: false -> false
According to the documentation the bounds should change with the orientation so it should have a before/after bounds, shouldn't it?
UIScreen main bounds:
This rectangle is specified in the current coordinate space, which
takes into account any interface rotations in effect for the device.
Therefore, the value of this property may change when the device
rotates between portrait and landscape orientations.
Whereas it works fine both iPhone and iPad if I use the bounds of the current root view controller like this:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
let isLand = UIApplication.shared.keyWindow!.rootViewController!.view.bounds.width > UIApplication.shared.keyWindow!.rootViewController!.view.bounds.height
coordinator.animate(alongsideTransition: nil) { _ in
let isLand2 = UIApplication.shared.keyWindow!.rootViewController!.view.bounds.width > UIApplication.shared.keyWindow!.rootViewController!.view.bounds.height
print("\(isLand) -> \(isLand2)")
}
}
Portrait > Landscape: false -> true
Landscape > Portrait: true -> false
You should try using the containerView of the coordinator context instead.
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
let isLand = coordinator.containerView.bounds.width > coordinator.containerView.bounds.height
coordinator.animate(alongsideTransition: nil) { _ in
let isLand2 = coordinator.containerView.bounds.width > coordinator.containerView.bounds.height
print("\(isLand) -> \(isLand2)")
}
}
If you want to get further information regarding the transition you can use the func view(forKey: UITransitionContextViewKey) and func viewController(forKey: UITransitionContextViewControllerKey) using the .from key.
in iOS9 when I rotate my screen with a UIPopoverPresentationController the coordinates are reset to 0 and not attached to my sourceView which is a button.
I have tried:
func popoverPresentationController(popoverPresentationController: UIPopoverPresentationController, willRepositionPopoverToRect rect: UnsafeMutablePointer, inView view: AutoreleasingUnsafeMutablePointer) {
rect.initialize(CGRectMake(200, 200, 400, 400))
}
but no avail. Any help ?
You could apply constraints to you sourceView button, then just use it as the popover source:
myPopoverViewController.popoverPresentationController?.sourceView = button
You also can set sourceView in the storyboard, but sourceRect needs to be set in code:
myPopoverViewController.popoverPresentationController?.sourceRect = button.bounds
You need the correct source rect for the popover arrow to align properly.
Now you don't need to dismiss and present the popover on rotation. UIPopoverPresentationController does that for you. You don't even need to update sourceView/sourceRect once they are set on creating the popover.
Use viewWillTransition to catch size and orientation changes:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: { _ in
if self.popover != nil {
// optionally scroll to popover source rect, if inside scroll view
let rect = ...
self.scrollView.scrollRectToVisible(rect, animated: false)
// update source rect constraints
buttonConstraint.constant = ...
button.setNeedsLayout()
button.layoutIfNeeded()
}
}, completion: nil)
}
Explanation:
The trick with animate(alongsideTransition: ((UIViewControllerTransitionCoordinatorContext) -> Void)?, completion: ((UIViewControllerTransitionCoordinatorContext) -> Void)? = nil) is that you should update your constraints in alongsideTransition closure, not in completion. This way you ensure that UIPopoverPresentationController has the updated sourceRect when restoring the popover at the end of rotation.
What might seem counter-intuitive is that inside alongsideTransition closure you already have your new layout that you derive your constraints calculation from.
I have a full-screen UIView. When I rotate the phone the view shrinks a little (and you see the black background) and then expands again to be full-screen. It basically animates as expected.
The other views on the screen also animate accordingly.
Can I prevent this from happening for one specific view?
I would like the full-screen view to just stay full-screen without animation revealing the black background, but maintain that the other views animate their rotation.
Kind of like how Apple does it in the camera app. The "viewfinder" does not animate its rotation, but the buttons do.
I have the following code in my ViewController. liveView is the the full-screen UIView mentioned.
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animateAlongsideTransition(in: self.liveView, animation: { (context) in
if let connection = (self.liveView.layer as! AVCaptureVideoPreviewLayer).connection {
connection.videoOrientation = AVCaptureVideoOrientation(ui: UIApplication.shared.statusBarOrientation)
}
}, completion: nil)
}
AVCaptureVideoOrentation is an extension which basically translates UIInterfaceOrientation to AVCaptureVideoOrientation with a switch statement.
Thanks
- Joseph
Did you try self.liveView.layer.removeAllAnimations():
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animateAlongsideTransition(in: self.liveView, animation: { (context) in
if let connection = (self.liveView.layer as! AVCaptureVideoPreviewLayer).connection {
connection.videoOrientation = AVCaptureVideoOrientation(ui: UIApplication.shared.statusBarOrientation)
}
self.liveView.layer.removeAllAnimations()
}, completion: nil)
}