I have two classes which subclass from UIViewController. PortraitViewController and LandscapeViewController.
These classes have their vars shouldAutorotate, supportedInterfaceOrientations and preferredInterfaceOrientationForPresentation with override implementation so they can stick to portrait or landscape orientation accordingly.
My application accepts Portrait and Landscape mode.
Now, I also have:
ViewController1 which subclasses from PortraitViewController
ViewController2 which subclasses from LandscapeViewController
When I show ViewController1 with a UINavigationController attached to it, it sticks with portrait as expected.
When I show ViewController2 with a UINavigationController attached to it, as a modal nav on top of ViewController1, it sticks with landscape as expected.
There's an extension for UINavigationController which also overrides the vars mentioned above, but it reads properties from the visibleController parameter.
But when I dismiss ViewController2, ViewController1 appears in landscape.
How can I make ViewController1 stick in portrait mode, regardless of what I show on top of it?
Note: Every time the device is in portrait mode.
EDIT:
Here's a demo project: https://www.dropbox.com/s/ishe88e1x81nzlp/DemoOrientationApp.zip?dl=0
The visibleViewController is either the top view controller of the navigation stack or a view controller presented from the navigation controller.
So, when you present the second navigation controller, the first navigation controller reads the second navigation controller property which passes back the landscape view controller's property.
What you need to use instead is topViewController. That way, the setting is limited to the view controller stack.
Okay, so:
Throw away your UINavigationControllerExtension.
Change your Info.plist so that we launch only into portrait:
(Delete items 1 and 2.)
In the app delegate, add this code, to allow us to rotate to landscape after launch:
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
return .all
}
In ViewController, fix the code as follows:
class ViewController: PortraitViewController, UINavigationControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.delegate = self
}
func navigationControllerSupportedInterfaceOrientations(_ navigationController: UINavigationController) -> UIInterfaceOrientationMask {
return self.supportedInterfaceOrientations
}
#IBAction func showView(_ sender: Any) {
let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "ViewController2") as! ViewController2
let nav = UINavigationController(rootViewController: vc)
nav.delegate = vc
present(nav, animated: true, completion: nil)
}
}
In ViewController2, fix the code as follows:
class ViewController2: LandscapeViewController, UINavigationControllerDelegate {
func navigationControllerSupportedInterfaceOrientations(_ navigationController: UINavigationController) -> UIInterfaceOrientationMask {
return self.supportedInterfaceOrientations
}
func navigationControllerPreferredInterfaceOrientationForPresentation(_ navigationController: UINavigationController) -> UIInterfaceOrientation {
return self.preferredInterfaceOrientationForPresentation
}
#IBAction func dismiss(_ sender: Any) {
dismiss(animated: true, completion: nil)
}
}
The app now behaves as desired.
Related
I have what seems to be a very common setup in my universal application, with a root UISplitViewController, using a UITabBarController as a masterViewController, and then I want to:
either push the detail view controller onto the stack if I'm on a vertical iPhone
show the detail controller in the detailViewController of the UISplitViewController on lanscape iPhone 6+ and other larger screens like iPads and such
To that effect, I have exactly the same setup as the ones described in all those discussions that mention a similar issue:
UINavigationController inside a UITabBarController inside a UISplitViewController presented modally on iPhone
iOS8 TabbarController inside a UISplitviewController Master
Adaptive show detail segue transformed to modal instead of push on iPhone when master view controller is a UITabBarController
But none of the solutions mentioned in those questions works. Some of them create an infinite recursive loop and an EXC_BAD_ACCESS. And the latest one I tried simply keeps presenting the detail view controller modally instead of pushing it onto the stack on iPhones. What I did is create a custom UISplitViewController subclass as such:
class RootSplitViewController: UISplitViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
}
}
extension RootSplitViewController: UISplitViewControllerDelegate {
func splitViewController(_ splitViewController: UISplitViewController, showDetail vc: UIViewController, sender: Any?) -> Bool {
if let tabController = splitViewController.viewControllers[0] as? UITabBarController {
if(splitViewController.traitCollection.horizontalSizeClass == .compact) {
tabController.selectedViewController?.show(vc, sender: sender)
} else {
splitViewController.viewControllers = [tabController, vc]
}
}
return true
}
func splitViewController(_ splitViewController: UISplitViewController, separateSecondaryFrom primaryViewController: UIViewController) -> UIViewController? {
if let tabController = splitViewController.viewControllers[0] as? UITabBarController {
if let navController = tabController.selectedViewController as? UINavigationController {
return navController.popViewController(animated: false)
} else {
return nil
}
} else {
return nil
}
}
}
And here is the code in the master view controller to show the detail view controller:
self.performSegue(withIdentifier: "showReference", sender: ["tags": tags, "reference": reference])
Where tags and reference where loaded from Firebase. And of course the "showReference" segue is of the "Show Detail (e.g. Replace)" kind.
The first delegate method is called correctly, as evidenced by the breakpoint that gets hit there when I click an item in the list inside the UITabBarController. And yet the detail view controller still presents modally on iPhone. No problem on iPad though: the detail view controller appears on the right, as expected.
Most of the answers mentioned above are pretty old and some of the solutions are implemented in Objective-C so maybe I did something wrong in the conversion, or something changed in the UISplitViewController implementation since then.
Does anyone have any suggestion?
I figured it out. In fact, it was related to the target view controller I was trying to show. Of the 2 methods I was overriding in UISplitViewControllerDelegate, only the first one was called:
func splitViewController(_ splitViewController: UISplitViewController, showDetail vc: UIViewController, sender: Any?) -> Bool {
if let tabController = splitViewController.viewControllers[0] as? UITabBarController {
if(splitViewController.traitCollection.horizontalSizeClass == .compact) {
tabController.selectedViewController?.show(vc, sender: sender)
} else {
splitViewController.viewControllers = [tabController, vc]
}
}
return true
}
But the view controller I was showing in the first branch of the test was already embedded into a UINavigationController, so I was essentially showing a UINavigationController into another one, and in that case the modal made more sense. So in that case I needed to show the top view controller of the UINavigationController, which I assume was the purpose of the second method I'm overriding in the delegate, but it was never called. So I did it right there with the following implementation:
extension RootSplitViewController: UISplitViewControllerDelegate {
func splitViewController(_ splitViewController: UISplitViewController, showDetail vc: UIViewController, sender: Any?) -> Bool {
if let tabController = splitViewController.viewControllers[0] as? UITabBarController {
if(splitViewController.traitCollection.horizontalSizeClass == .compact) {
if let navController = vc as? UINavigationController, let actualVc = navController.topViewController {
tabController.selectedViewController?.show(actualVc, sender: sender)
navController.popViewController(animated: false)
} else {
tabController.selectedViewController?.show(vc, sender: sender)
}
} else {
splitViewController.viewControllers = [tabController, vc]
}
}
return true
}
}
And that seems to work perfectly, both on iPhones and iPads
Having you tried ShowDetailViewController Method to change the detail view controller in split view controller.
splitViewController.showDetailViewController(vc, sender: self)
If in case your view controller does not contain navigation controller you can also embed it in a navigation controller.
let nav = UINavigationController.init(rootViewController: vc)
splitViewController.showDetailViewController(nav, sender: self)
I have a simple camera app.
The Camera Preview screen is a viewcontroller (name ViewController) which I locked the orientation via
this code (can't find the StackOverflow link to this code), The viewcontroller is embedded in a navigation controller:
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
if let navigationController = self.window?.rootViewController as? UINavigationController {
if navigationController.visibleViewController is ViewController {
return UIInterfaceOrientationMask.portrait
} else {
return UIInterfaceOrientationMask.all
}
}
return UIInterfaceOrientationMask.portrait
}
When the camera starts up, the Camera Preview is locked in .portrait orientation, which is what I desire.
However, if I hit the library button, which segues me to another screen,
and change the orientation of the library screen by rotating my physical device, and THEN return back to my Camera Preview screen via the "back" button, the orientation is broken. It's in landscape mode, instead of portrait mode.
How do I get a hard lock?
Just incase, I made a dummy model of my code here (too long to post here), where you guys can test it out yourself
https://github.com/bigmit2011/Testing-Camera
EDIT:
If the app crashes just try running it one more time.
For some reason the first time the app is ran it crashes. Thank you.
EDIT2:
Attempting to set embedded ViewController as a delegate of UINavigationController.
However, I don't quite understand if I'm doing it correctly:
class ViewController: UIViewController, UINavigationControllerDelegate {
var navigation: UINavigationController?
self.navigation.delegate = self // doesn't seem to work
func navigationControllerSupportedInterfaceOrientations(_ navigationController: UINavigationController) -> UIInterfaceOrientationMask {
return .portrait
}
Do I need to create a separate swift file for the UiNavigationController?
EDIT3:
Following Matt's answer code is as follows:
class CameraViewController :UIViewController, UINavigationControllerDelegate {
override func viewDidLoad() {
self.navigationController?.delegate = self
super.viewDidLoad()
func navigationControllerSupportedInterfaceOrientations(_ navigationController: UINavigationController) -> UIInterfaceOrientationMask {
return .portrait
}
However, this seems to lock everything in portrait mode not just that single viewController embedded within Navigation Controller.
You are making two mistakes.
First, you are trying to govern the orientation of individual view controllers from the app delegate. That's wrong. The app delegate governs the totality of possible orientations for the whole app. But as for view controllers, each individual view controller governs its own orientation. Just implement supportedInterfaceOrientations in each of your two view controllers (not in the app delegate).
Second, your first view controller is in a navigation interface. Therefore it is not the top-level view controller and cannot govern orientation directly (with supportedInterfaceOrientations). The correct way to govern the orientation of a navigation interface is to set yourself as the navigation controller's delegate and implement navigationControllerSupportedInterfaceOrientations(_:).
I also couldn't run your app. But I tested this in Xcode with a main VC, a 2nd VC and embedded the main VC in a navigation controller. I then followed the advice in this article Selective rotation lock in iOS
You add the following to AppDelegate:
var enableAllOrientation = false
func application(_ application: UIApplication,supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
if (enableAllOrientation == true){
return UIInterfaceOrientationMask.allButUpsideDown
}
return UIInterfaceOrientationMask.portrait
}
Then in the VC that allows allButUpsideDown (or change this to what you want), you add the following code:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.enableAllOrientation = true
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.enableAllOrientation = false
let value = UIInterfaceOrientation.portrait.rawValue
UIDevice.current.setValue(value, forKey: "orientation")
}
I put this in the 2nd VC. Ran the app in the simulator, segued to the 2nd VC, changed the orientation to landscape then hit back and the main VC was locked in portrait.
My App is basically a portrait only app. So nothing is rotating. But not there is ONE exception. The user can add photos and when viewing those photos full-screen, this ViewController SHOULD be allowed to rotate.
So I thought that if my ViewController that is presenting has supportedInterfaceOrientations return .portrait and also shouldAutorotate return false, that this should be enough, to prevent that one from rotation?!?
Turns out, that when I rotate while having the full-screen image presented, the one underneath is rotated as well.
To summarize:
RootViewController should NEVER rotate
PresentedViewController can rotate, but his rotation should no rotate the RootViewController
Is there a way to achieve that?
Probably too late, but in case if somebody will be faced with the same issue, i would provide my solution.
Actually suppress rotation of underlying window is possible if set modalPresentationStyle = .fullScreen for presented controller, then if you take a look of "View UI Hierarchy" then you can notice that controller which represents controller in fullscreen will be removed from controllers hierarchy. But at the same time modalPresentationStyle = .overFullScreen keep everything as is, what causing rotating underlying controller even it set by default supported orientation to portrait, i.e. UIWindow who manages and routes system events over hierarchy respect settings of the toppest controller in case of modalPresentationStyle = .overFullScreen. So according to the facts, and if it is necessary to have e.g. custom presentation, i would suggest to use additional UIWindow which will be responsible for the presenting controller in fullscreen.
i have implemented test project for the solution: here
You can give an exception like in AppDelegate:
//auto rotate
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
//landscape for perticular view controller
let navigationController = window?.rootViewController as? UINavigationController
if let activeController = navigationController?.visibleViewController {
if activeController.isKind(of: VGVideoVC.self) {//Pass your VC here
// print("I have found my controller!")
return UIInterfaceOrientationMask.all;
}else{
return UIInterfaceOrientationMask.portrait;
}
}else{
return UIInterfaceOrientationMask.portrait;
}
}
And in the Rest of the VC where you want it to be forcefully portrait, you can use like this:
//MARK:- Screen Orientation
override var supportedInterfaceOrientations: UIInterfaceOrientationMask{
return .portrait
}
override var shouldAutorotate: Bool{
return true
}
Hope this helps.
I would say disable the orientation change for the complete app and listen to device orientation change in Photos view controller and update the UI of photosVC on device orientation change.
Something like this:
NotificationCenter.default.addObserver(self, selector: #selector(orientationChanged), name: Notification.Name("UIDeviceOrientationDidChangeNotification"), object: nil)
#objc func orientationChanged() {
if(UIDeviceOrientationIsLandscape(UIDevice.current.orientation)){
print("landscape")
}
if(UIDeviceOrientationIsPortrait(UIDevice.current.orientation)){
print("Portrait")
}
}
Be careful with the upside down and other orientations which you don't need.
Try this code below. I followed This tutorial and it works for me. What's going on is:
Step 1. Assuming inside General your Device Orientation is set to Portrait only:
Step 2. The code below that you add inside AppDelegate loops through the navigation controllers and then looks inside their top view controllers. If any of those vcs have a function with the name canRotate then that specific vc will change the device orientation from Step 1. by returning: return .allButUpsideDown
Add these 2 functions to the bottom of your AppDelegate:
// add this first function
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
// if the navigationController's root vc has a function inside of it named canRotate
if let rootViewController = self.topViewControllerWithRootViewController(rootViewController: window?.rootViewController) {
if (rootViewController.responds(to: Selector(("canRotate")))) {
// Unlock landscape view orientations for this view controller
return .allButUpsideDown;
}
}
// Only allow portrait (standard behaviour). vcs that don't contain a function with the name "canRotate" can't rotate and stay in portrait only
return .portrait;
}
// add this second function
// loop through tabBarController or any navigationControllers
private func topViewControllerWithRootViewController(rootViewController: UIViewController!) -> UIViewController? {
if (rootViewController == nil) { return nil }
if (rootViewController.isKind(of: UITabBarController.self)) {
return topViewControllerWithRootViewController(rootViewController: (rootViewController as! UITabBarController).selectedViewController)
} else if (rootViewController.isKind(of: UINavigationController.self)) {
return topViewControllerWithRootViewController(rootViewController: (rootViewController as! UINavigationController).visibleViewController)
} else if (rootViewController.presentedViewController != nil) {
return topViewControllerWithRootViewController(rootViewController: rootViewController.presentedViewController)
}
return rootViewController
}
Step 3. Inside the modal vc you should add a function named: #objc func canRotate(){}. You don't have to call it anywhere or add anything inside it's curly braces. The code from Step 2 is looking for this function with the name canRotate. If the other vcs don't contain a function with that name then they can't rotate.
Inside the modal viewController that you want to rotate add the canRotate() function anywhere outside of viewDidLoad and inside viewWillDisappear add the code to set everything back to your regular Portrait only :
override func viewDidLoad() {
super.viewDidLoad()
}
#objc func canRotate(){}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// add this so once leaving this vc everything will go back to Portrait only
if (self.isMovingFromParentViewController) {
UIDevice.current.setValue(Int(UIInterfaceOrientation.portrait.rawValue), forKey: "orientation")
}
}
I have a navigation controller with a rootviewcontroller with portrait orientation. Then i want to push a second viewcontroller to the stack with landscape orientation. Sadly i found no way to force the app to recheck supportedInterfaceOrientations. So the landscape viewcontroller is shown in protrait until the user rotates his device into landscape.
I prepared a test project: https://github.com/buechner/InterfaceOrientationTest
Is it even possible to automatically change the orientation within a navigationcontroller stack?
You can present a ViewController within LandscapeViewController to forcefully make it landscape
Use following line of the code in your viewDidLoad of LandscapeViewController
self.performSelector(#selector(LandscapeViewController.launchLandscapeScreen), withObject: nil, afterDelay:1)
add following method in your LandscapeViewController
func launchLandscapeScreen() -> Void{
let viewController = UIViewController()
self.presentViewController(viewController, animated: false, completion: nil)
self.dismissViewControllerAnimated(false, completion: nil)
}
It could be done but you'll have to make UIStoryboardSegue subclass
Make a subclass of UIStoryBoradSegue and override perform()
override func perform() {
let sourceVC = self.sourceViewController
let destonationVC = self.destinationViewController
sourceVC.navigationController!.presentViewController(destonationVC, animated: true, completion: nil)
}
In your NavigationControllerSubclass change shouldAutoRotate and supportedInterfaceOrientations
public override func shouldAutorotate() -> Bool {
return (self.topViewController?.shouldAutorotate())!
}
public override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
return (self.topViewController?.supportedInterfaceOrientations())!
}
while connecting Segue from viewControllers select your segue subclass
4.add shouldAutorotate and supportedInterfaceOrientations methods in every class to
override func shouldAutorotate() -> Bool {
return true
}
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
return .Portrait // or landscape in case you want orientation to be landscape
}
As we're making custom segue, you also need to add back button.
In XCode 6, if you create a new project based on the Master-Detail Application template, you get a universal storyboard that is supposed to be good for all devices.
When selecting a cell in the master view, the detail view is updated via an adaptive "show detail" segue. On an iPhone 4, 5, 6 or 6+ in portrait, this segue will take the form of a push as expected. On an iPad or an iPhone 6+ in landscape, it will cause the detail view to be updated as expected.
Now, if you insert a UITabBarController as the master view controller which has a tab to the original master view controller, the adaptive segue that occurs when selecting a cell in the master view does not behave as expected on iPhones. Instead of getting a push transition, you now get a modal transition. How can I fix that? Seems odd that this is not supported by default.
I found the following post useful: iOS8 TabbarController inside a UISplitviewController Master
But when using the suggested method, I don't get the right behaviour on an iPhone 6 Plus when I rotate to landscape after a push in portrait. The content of the detail view appears in the master view which is not surprising since that's what the suggested solution does.
Thanks!
Re-watching videos from WWDC14 I think I've found a better answer.
Use a custom UISplitViewController (subclass)
Override the showDetailViewController operation
Use the traitCollection to determine the class of the UISplitViewController
If the horizontal class is Compact, get the navigationController to call showViewController
Here is the the code of the custom UISplitViewController :
import UIKit
class CustomSplitViewController: UISplitViewController {
override func showDetailViewController(vc: UIViewController!, sender: AnyObject!) {
if (self.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClass.Compact) {
if let tabBarController = self.viewControllers[0] as? UITabBarController {
if let navigationController = tabBarController.selectedViewController as? UINavigationController {
navigationController.showViewController(vc, sender: sender)
return
}
}
}
super.showDetailViewController(vc, sender: sender)
}
}
Do not forget to the set the custom class in the storyboard.
Tested in the simulator of iPhone 6, iPhone 6+ and iPad Air and worked as expected.
Unfortunately, the selected answer didn't work for me. However, I did eventually manage to solve the problem:
Subclass UISplitViewController and set the new class in Interface Builder.
Make the new class conform to UISplitViewControllerDelegate:
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.delegate = self
}
Implement these two methods:
func splitViewController(_ splitViewController: UISplitViewController,
collapseSecondary secondaryViewController:UIViewController,
onto primaryViewController:UIViewController) -> Bool {
return true
}
func splitViewController(_ splitViewController: UISplitViewController,
showDetail vc: UIViewController,
sender: Any?) -> Bool {
if splitViewController.isCollapsed {
guard let tabBarController = splitViewController.viewControllers.first as? UITabBarController else { return false }
guard let selectedNavigationViewController = tabBarController.selectedViewController as? UINavigationController else { return false }
// Push view controller
var detailViewController = vc
if let navController = vc as? UINavigationController, let topViewController = navController.topViewController {
detailViewController = topViewController
}
selectedNavigationViewController.pushViewController(detailViewController, animated: true)
return true
}
return false
}
The docs state when the split controller is collapsed, it handles showDetail by calling show on the master view controller, which in your case is a tab controller. You need to forward that on to the child nav controller as follows:
Make a tab controller subclass.
In the storyboard set the tab controller to use the new subclass.
Add this method to the subclass:
- (void)showViewController:(UIViewController *)vc sender:(id)sender{
[self.viewControllers.firstObject showViewController:vc sender:sender];
}
This forwards it on to the nav controller in the first tab.