iOS - Relaunch app on orientation change? - ios

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.

Related

override traitCollection in iOS 13

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()
}
}

UIViewController selective Autorotation with Size classes

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.

Preventing rotation in a specific view controller in iOS 11

I am trying to prevent rotation (lock it to say, portrait) in a specific VC that is
embedded in a navigation controller.
I am currently doing this:
To UINavigationController
extension UINavigationController {
public override func supportedInterfaceOrientations() -> Int {
return visibleViewController.supportedInterfaceOrientations()
}
}
In my VC:
class ViewController: UIViewController {
override func supportedInterfaceOrientations() -> Int {
return Int(UIInterfaceOrientationMask.Landscape.rawValue)
}
}
However, I have issues when we go to another VC (embedded in another nav controller) is presented which supports both landscape and portrait. Suppose, the user rotates in the new screen to landscape. And clicks back to go to original screen. The app is now presented in landscape as opposed to portrait defined in its supportedInterfaceOrientations override. How do I prevent this erroneous behaviour?
I read in iOS 11, we should use viewWillTransition(to:with:) to handle rotation (and locking as well). In UIViewController documentation
“As of iOS 8, all rotation-related methods are deprecated. Instead,
rotations are treated as a change in the size of the view controller’s
view and are therefore reported using the viewWillTransition(to:with:)
method. When the interface orientation changes, UIKit calls this
method on the window’s root view controller. That view controller then
notifies its child view controllers, propagating the message
throughout the view controller hierarchy.”
Can you give directions on how to achieve it?
You can use this cool utility that I've been using.
struct AppUtility {
static func lockOrientation(_ orientation: UIInterfaceOrientationMask) {
if let delegate = UIApplication.shared.delegate as? AppDelegate {
delegate.orientationLock = orientation
}
}
/// OPTIONAL Added method to adjust lock and rotate to the desired orientation
static func lockOrientation(_ orientation: UIInterfaceOrientationMask, andRotateTo rotateOrientation:UIInterfaceOrientation) {
self.lockOrientation(orientation)
UIDevice.current.setValue(rotateOrientation.rawValue, forKey: "orientation")
}
}
You can also rotate your screen and at the same time, lock it to that orientation. I hope this helps!
You can use supportedInterfaceOrientationsFor
Define this in your AppDelegate
var restrictRotation = Bool()
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
if restrictRotation {
return .portrait
}
else {
return .all
}
}
Put below code in your ViewController
func restrictRotation(_ restriction: Bool) {
let appDelegate = UIApplication.shared.delegate as? AppDelegate
appDelegate?.restrictRotation = restriction
}
call above function in your ViewController ViewwillAppear like this.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.restrictRotation(true) // TRUE MEANS ONLY PORTRAIT MODE
//OR
self.restrictRotation(false) // FALSE MEANS ROTATE IN ALL DIRECTIONS
}

How to prevent ARSCNView from rotating during orientation change

I am using ARKit and I want to allow the users to use the app in both portrait and landscape mode.
I would like all UI controls to rotate on orientation change except for the ARSCNView.
I tried to transform the sceneView in the opposite direction but that didn't work.
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
let targetRotation = coordinator.targetTransform
let inverseRotation = targetRotation.inverted()
coordinator.animate(alongsideTransition: { (context) in
self.sceneView.transform = self.sceneView.transform.concatenating(inverseRotation)
context.viewController(forKey: UITransitionContextViewControllerKey.from)
}, completion: nil)
}
How can I prevent the scene view of the ARKit session from rotating while allowing all other UI controls to rotate on orientation change?
You cannot specify device rotation rules on view basis. It has to be set on view controller basis. This is how iOS works. Thus, to achieve what you need you have to handle this by yourself. For example, if you're showing your ARSCNView as a full screen view, then you can present it inside a custom UIViewController sub-class, and set the rotation configuration for that controller.
Setting the supported view rotations for a specific view controller can be implemented in many ways, below are some of them.
Approach #1:
You can set the supported view orientations for any UIViewController by overriding your app delegate's method application:supportedInterfaceOrientationsForWindow:.
Sample code:
// this goes into your AppDelegate...
// Basically, this checks the current visible view controller type and decide
// what orientations your app supports based on that view controller type (class)
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
let visibleViewController = self.topViewController(withRootViewController: window?.rootViewController)
// Support only portrait orientation for a specific view controller
if visibleViewController is SomeViewController {
return .portrait
}
// Otherwise, support all orientations (standard behaviour)
return .allButUpsideDown
}
// This simple helper method is to extract the exact visible view controller at
// the moment, as `window.rootViewController` could have some container controller
// like `UINavigationController` or so that holds more controllers into it
private func topViewController(withRootViewController rootViewController: UIViewController?) -> UIViewController? {
if let rootViewController = rootViewController as? UITabBarController {
return rootViewController.selectedViewController
} else if let rootViewController = rootViewController as? UINavigationController {
return rootViewController.visibleViewController
} else if let presentedViewController = rootViewController?.presentedViewController {
return topViewController(withRootViewController: presentedViewController)
}
return rootViewController
}
Reference:
application:supportedInterfaceOrientationsForWindow: method documentation
Approach #2:
Sub-class your UINavigationController and override the following property:
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return self.topViewController?.supportedInterfaceOrientations ?? .all
}
Now this always looks into your view controller's supportedInterfaceOrientations and set the supported orientations based on the return value. Doing this enables you to simply override this in any view controller you want, setting some custom value.
For example, in SomeViewController you could simply add:
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .portrait
}
Reference:
supportedInterfaceOrientations property documentation
Approach #3:
If you don't want to sub-class your UINavigationController like in approach #2 above, you can set your SomeViewController as the navigation controller delegate implementing navigationControllerSupportedInterfaceOrientations(:)
Sample code:
override func viewDidLoad() {
super.viewDidLoad()
// ...
self.navigationController?.delegate = self
}
// MARK: - UINavigationControllerDelegate
func navigationControllerSupportedInterfaceOrientations(_ navigationController: UINavigationController) -> UIInterfaceOrientationMask {
return navigationController.topViewController?.supportedInterfaceOrientations ?? .all
}
Reference:
Matt's answer here

viewWillTransitionToSize Calls Wrong ViewController in TabBarController

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...

Resources