UISplitViewController iPad behaviour - ios

I'm making my first universal app, so far so good, but I have a problem with UISplitViewController on iPad.
how can i make the UISplitViewController act same as on iPhone when it is in portrait mode?
like in portrait mode show only master screen when i click on it, it navigate to the details screen, and when in landscape mode show both of them beside each other.
what happens now , is that it shows the details screen only in portrait and show both of them in landscape mode.
for iPhone i used this code in master view to solve this issue
func splitViewController(splitViewController: UISplitViewController, collapseSecondaryViewController secondaryViewController: UIViewController, ontoPrimaryViewController primaryViewController: UIViewController) -> Bool {
return true
}
but this didn't work with iPad, i found another code on here but didn't work too.
func splitViewController(svc: UISplitViewController, willHideViewController aViewController: UIViewController, withBarButtonItem barButtonItem: UIBarButtonItem, forPopoverController pc: UIPopoverController) {
self.navigationItem.leftBarButtonItem?.target?.performSelector((self.navigationItem.leftBarButtonItem?.action)!, withObject: self.navigationItem)
}
other code maybe you need to know, i added those in viewDidLoad in master view controller
self.splitViewController?.delegate = self
self.splitViewController?.preferredDisplayMode = UISplitViewControllerDisplayMode.PrimaryOverlay
self.splitViewController!.maximumPrimaryColumnWidth = splitViewController!.view.bounds.size.width;
self.splitViewController!.preferredPrimaryColumnWidthFraction = 0.3
so please if anyone can help me find solution for this issue, I will be very thankful

UISplitViewController use size classes to determine how to display his master and detail view controller.
When your UISplitViewController has horizontalSizeClass and verticalSizeClass Regular it will display both the Master and Detail view controllers on the same screen.
You'll need to embed your split view controller into a container view controller to override the default size class as I explained here.
You also have to check the device orientation to fork between Compact (when Portrait) or Regular (when Landscape) horizontal size class:
class ContainerVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
performOverrideTraitCollection()
}
private func performOverrideTraitCollection() {
let orientation = UIDevice.currentDevice().orientation
var isPortrait = false
switch orientation {
case .Portrait, .PortraitUpsideDown:
isPortrait = true
default:
isPortrait = false
}
for childVC in self.childViewControllers {
self.traitCollection.userInterfaceIdiom
setOverrideTraitCollection(UITraitCollection(horizontalSizeClass: isPortrait ? .Compact : .Regular), forChildViewController: childVC)
}
}
}

Related

How to restore right orientation of the UIViewController after orientation was forced to landscape by another (iOS)?

The problem I am having is a bit strange and I can't seem to find any good solutions out there. So I have two UIViewControllers, one is allowed to take all orientations, another can only be viewed in landscape
internal override func shouldAutorotate() -> Bool {
return true
}
internal override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
return UIInterfaceOrientationMask.Lanscape
}
If I am in portrait mode on ViewController1, and I push VC2, it rotates just fine, but when I leave VC2, VC1 is stuck in landscape even though device itself is in portrait mode. How can I fix it? I tried calling AttemptToRotateToDeviceOrientation method in ViewDidAppear method of VC1, but it doesn't do anything
You just need to create app delegate instance in your view controller like:
let appDel = UIApplication.shared.delegate as! AppDelegate
On Viewwillappear()of view controller just add this code for landscape
appDel.myOrientation = .landscape
UIDevice.current.setValue(UIInterfaceOrientation.landscapeLeft.rawValue, forKey: "orientation")
And for portrait add this code
appDel.myOrientation = .portrait
UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")

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

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

Lock one view orientation in iOS

I'm trying to lock one view of my app in portrait mode. I have a top navigation controller view and multiple nested scenes.
I found many answers to lock orientation question and tried the code below, but it works only partially. Let the scenes A and B, where A has a segue to B, and B is locked in portrait orientation. If I go to the B scene, turn the device to the landscape position and back to A, the orientation is inherited from previous B (portrait).
How can I make the A scene recognize the actual device orientation?
Navigation controller
internal override func shouldAutorotate() -> Bool {
return visibleViewController!.shouldAutorotate()
}
"B" View controller
override func shouldAutorotate() -> Bool {
if UIDevice.currentDevice().orientation == .Portrait {
return true
}
return false
}
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
return UIInterfaceOrientationMask.Portrait
}
override func viewDidLoad() {
super.viewDidLoad()
let orientation = UIInterfaceOrientation.Portrait.rawValue
UIDevice.currentDevice().setValue(orientation, forKey: "orientation")
}
As I understand your question you want to know how to get the current device orientation.
Try this...
var orientation = UIDevice.currentDevice().orientation

UINavigationController doesn't rotate when presenting child view controller

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.

Resources