I have tabBarController application with 4 viewcontrollers. This application is landscape orientation enabled so I have viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coordinator in each viewcontroller.m file to control the orientation changes.
The problem I'm having, is when I change the device orientation while in the 3rd viewcontroller, the viewWillTransitionToSize in the 2nd viewcontroller is called so the wrong code is ran.
How is it possible that the 2nd viewcontroller's viewWillTransitionToSize is even called? Especially, when it hasn't even been loaded yet. I know it hasn't been loaded because I NSLog it's viewDidLoad and it shows when I change orientation from the 3rd viewcontroller.
Additional Info: There is no code in the 3rd viewcontroller's viewWillTransitionToSize, viewWillAppear, viewWillDisappear, etc. that would reference the 2nd viewcontroller.
I'm using Xcode 8.2.1 and Objective-C code. Please help, thanks.
Test to see which UIViewController is the selected UIViewController before handling the transition.
In Swift:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator)
{
guard self == tabBarController?.selectedViewController else { return }
// handle transition here
}
In my situation, the UIViewController was embedded in a UINavigationController so I had to handle it slightly differently:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator)
{
guard self.navigationController == tabBarController?.selectedViewController else { return }
// handle transition here
}
I replaced each instance of
viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coordinator
with
willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
to avoid the aforementioned issue with viewWillTransitionToSize...
Related
On iOS, is it possible to relaunch an app automatically when the device orientation changes? Android has this feature (just make sure android:configChanges does not include orientation).
There are no methods available in iOS to restart your application, BUT you could manually reinstantiate initial root UIViewController when orientation changes.
To reinstantiate root UIViewController you could use following static functions in your AppDelegate class:
static func getWindow() -> UIWindow? {
return UIApplication.shared.keyWindow
}
static func resetViewController() {
let window = getWindow()
let controller = ... /// Instantiate your inital `UIViewController`
window?.rootViewController = controller
window?.makeKeyAndVisible()
}
To listen for orientation change use following method in UIViewController:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
AppDelegate.resetViewController() /// "Restarts" application
}
While this is a viable approach, I would highly discourage you from restarting your application when orientation change happens and just handle orientation changes how it supposed to be handled according to Apple.
In my initial view controller, I have have a UITabbarController as a child view controller.
I want to have UITabbarController to display its UITabbar with traitCollection having horizontalSizeClass of Compact so that in the tabbar, image and title appears vertically aligned and not side by side.
Overriding the traitCollection getter of UITabbarController is now not supported in iOS13,
Xcode gives below warning.
override var traitCollection: UITraitCollection{
let current = super.traitCollection
let compact = UITraitCollection(horizontalSizeClass: .compact)
return UITraitCollection(traitsFrom: [current, compact])
}
Class MyTabbarController overrides the -traitCollection getter, which is not supported. If you're trying to override traits, you must use the appropriate API.
After researching for appropriate API, I found
open func setOverrideTraitCollection(_ collection: UITraitCollection?, forChild childViewController: UIViewController)
After implementing this I am able to override trait collection of myTabbarController but only after the view has changed orientation. This API is only working if I override viewWillTransition to method.
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
let currentTC = traitCollection
let compactTC = UITraitCollection(horizontalSizeClass: .compact)
let custom = UITraitCollection(traitsFrom: [currentTC, compactTC])
print("ovverride trait collections before transition")
setOverrideTraitCollection(custom, forChild: tabController)
}
I am only able to override the traits when the device is rotated. This API is not working if I try to override the trait collection in any other view controller lifecycle method. How do I override the traitCollection when the view is initially loaded?
I tried using the same code in the viewDidLoad() method of my initial view controller but it has no effect.
I'm not sure if the OP ever got this working, but I ran into the same issue recently. In my case, I need to treat the device orientation the same for iPhone and iPad, and in particular set the horizontalSizeClass to .compact in portrait orientation.
Because setOverrideTraitCollection() only works on a child view controller, I had to embed my "master" view controller inside another view controller (which I call my "root" view controller), and handle the trait overrides in the root view controller. As the OP alluded to, this needs to happen at both app startup and whenever the orientation changes. In my case, I could do the startup code in prepareForSegue. Not sure why putting the code in viewDidLoad() didn't work for the OP -- perhaps because he wasn't calling setNeedsLayout() for the child view controller's view.
Here's my root view controller code:
class RootViewController: UIViewController {
var masterViewController: MasterViewController?
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "MasterViewSegue" {
masterViewController = segue.destination as? MasterViewController
updateMasterViewTraits(for: CGSize(width: view.bounds.width, height: view.bounds.height))
}
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
updateMasterViewTraits(for: size)
}
func updateMasterViewTraits(for size: CGSize) {
var orientationTraits: UITraitCollection
if size.width < size.height {
orientationTraits = UITraitCollection(traitsFrom:[UITraitCollection(horizontalSizeClass: .compact), UITraitCollection(verticalSizeClass: .regular)])
} else {
orientationTraits = UITraitCollection(traitsFrom:[UITraitCollection(horizontalSizeClass: .regular), UITraitCollection(verticalSizeClass: .compact)])
}
let traits = UITraitCollection(traitsFrom: [traitCollection, orientationTraits])
setOverrideTraitCollection(traits, forChild: masterViewController!)
masterViewController!.view.setNeedsLayout()
}
}
The new size classes based Autorotation is a pain if you want custom behavior. Here are my requirements and flow:
View Controller 1(VC1) modally presents View Controller 2 (VC2)
VC1 only supports landscape, portrait, or both depending on user settings (achieved via supportedInterfaceOrientations). For this example, we assume it is locked to landscape,
VC 2 supports both landscape & portrait
I use Size classes and in View Controller 1, I check for statusBarOrientation in viewWillTransition(to size...) to configure interface elements positioning & other customizations.
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
let orientation = UIApplication.shared.statusBarOrientation
NSLog("View will transition to \(size), \(orientation.rawValue)")
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: { [unowned self] (_) in
....
}
}
This works, except when VC 2 is presented and the device is rotated. VC 1 is all messed up when VC 2 is dismissed. To be sure, I would like to refresh layout of View Controller 1 when it appears. How do I do that? I tried UIViewController.attemptRotationToDeviceOrientation() but it doesn't force the autorotation methods to be called again.
EDIT: As can be seen, in VC1 I check for statusBarOrientation to determine interface orientation. That poses a problem, because statusBarOrientation gets changed to portrait when VC2 rotates to portrait mode. And viewWillTransition to size gets invoked on VC1 at the same time where I force layout to portrait mode.
My application has Tabbar and Navigation options. My problem is, when device is rotating to landscape, previous viewcontroller viewwilltransitiontosize is also calling
Let's explain the scenario,
The first screen is AssignmentViewController
The second screen is SubmissionListViewController
Both class I have override viewwilltransitiontosize method. when I rotate to landscape in the 2nd view controller time, it first calls AssignmentViewController's viewwilltransitiontosize method then it calls SubmissionListViewController's viewwilltransitiontosize
here is my piece of code
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
if UIDevice.currentDevice().orientation.isLandscape.boolValue {
print("Landscape")
self.navigationController?.setNavigationBarHidden(true, animated: false)
self.landscapeVideo()
self.tabBarController!.tabBar.hidden = true
} else {
print("Portrait")
self.navigationController?.setNavigationBarHidden(false, animated: false)
self.videoinPortraitMode()
self.tabBarController!.tabBar.hidden = false
}
}
any idea or help please?
Here is my sample project
I have two view controllers embedded in a UINavigationController. On the first one, there is just a button performing a segue on the 2nd view controller. On the latter, a button dismiss it back to the 1rst view controller.
The 1rst view controller is not allowed to rotate and stays in Portrait while the 2nd is allowed to rotate in Landscape.
To do so, I added this code in the 1rst view controller:
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
return UIInterfaceOrientationMask.Portrait
}
and added an extension to UINavigationController:
extension UINavigationController {
override public func shouldAutorotate() -> Bool {
if let topViewController = topViewController {
return topViewController.shouldAutorotate()
}
return false
}
override public func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
if let topViewController = self.topViewController {
return topViewController.supportedInterfaceOrientations()
}
return .Portrait
}
}
On the 2nd view controller, I add programmatically a label with some autolayout constraints. The label's title show the UIDevice.currentDevice().orientation.
My problem is the following:
When I put the device on landscape when I'm on the 1rst view controller, it's fine, the layout is laid for Portrait but when I tapp the button to present the 2nd view controller, this one stays on Portrait instead of switching to Landscape.
And bigger problem for me as in my real project I set some constraints depending on the device orientation, the UIDevice.currentDevice().orientation return the Landscape position.
What's wrong? Is it a normal behaviour? How can I fix it?
In your sample project, you're not updating the label when the device is rotated. You should override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) Also, Apple recommends that you not use UIDevice orientation but rather just look at the bounds of your view controller's view. For example:
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
if size.height<size.width {
label.text = "Landscape"
} else {
label.text = "Portrait"
}
}
Calling UIViewController.attemptRotationToDeviceOrientation() in the viewWillAppear of the 2nd view controller did the trick.