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()
}
}
Related
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
I'm trying to define a popover view attached to a view like this:
Here's my code:
class MyController: UIViewController, UIPopoverPresentationControllerDelegate {
...
func displaySignOut(_ sender: UIButton) {
let vc = UIStoryboard(name: "Main", bundle: nil)
.instantiateViewController(withIdentifier: "signOutPopover")
vc.modalPresentationStyle = .popover
vc.preferredContentSize = CGSize(width: 100, height: 30)
present(vc, animated: true, completion: nil)
let pc = vc.popoverPresentationController!
pc.sourceView = sender
pc.sourceRect = sender.bounds
pc.delegate = self
}
func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle {
return .none
}
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return .none
}
}
Because the popover is so small, I'd like to use this style on all devices. I've followed the usual advice (e.g., here) on overriding adaptivePresentationStyle to return UIModalPresentationStyle.none.
This works fine on iPad devices, but on iPhones, it doesn't. On smaller iPhone devices, it comes up full screen all the time. On larger screens (e.g., iPhone 7 Plus), it comes up wrong, but, weirdly, switches to a popover presentation (in both portrait and landscape) if I rotate the device after the popover appears. (If I dismiss the popover and bring it up again, it's wrong again until I rotate the device.) Furthermore, in landscape it comes up in a strange configuration (not full screen as in portrait):
Unlike with a popover presentation, this does not dismiss if I tap outside the popover view itself.
The Apple documentation says (in part):
In a horizontally compact environment, popovers adapt to the UIModalPresentationOverFullScreen presentation style by default.
The "by default" strongly suggests that there's a way to override this behavior. But (as is consistent with this post), overriding adaptivePresentationStyle in the delegate doesn't seem to be the way to do this any more (although it used to work). So is there a new way to modify the default behavior?
I'm using XCode 8.3.3 and Swift 3.1, targeting iOS 9+.
I have created one custom class with storyboard inside that connect
outlet of button and implemented below code.
import UIKit
class PopOverViewController: UIViewController {
#IBOutlet weak var button: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
button.backgroundColor = UIColor.purple
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
//Updating the popover size
override var preferredContentSize: CGSize {
get {
let size = CGSize(width: 80, height: 60)
return size
}
set {
super.preferredContentSize = newValue
}
}
//Setup the ViewController for popover presentation
func updatePopOverViewController(_ button: UIButton?, with delegate: AnyObject?) {
guard let button = button else { return }
modalPresentationStyle = .popover
popoverPresentationController?.permittedArrowDirections = [.any]
popoverPresentationController?.backgroundColor = UIColor.purple
popoverPresentationController?.sourceView = button
popoverPresentationController?.sourceRect = button.bounds
popoverPresentationController?.delegate = delegate
}
}
And then Inside ViewController implemented one function to show
popOver on iphone
func showPopOver(button: UIButton!) {
let viewController = PopOverViewController()
viewController.updatePopOverViewController(button, with: self)
present(viewController, animated: true, completion: nil)
}
Note:- Tested and this should work on Portrait as well Landscape mode
iOS 15 has some new ways to solve this problem.
Take a look at the WWDC21 Session "Customize and Resize Sheets in UIKit" https://developer.apple.com/wwdc21/10063
Pretty simple new interface for popovers and customized sheets. Shows how to do non-modal interaction with pop over and the view behind it.
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)
}
}
}
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.
I tried to implement UISplitViewController by following steps in 《iOS 8 by tutorial》。
The ducoment said if I return yes in splitViewController:collapseSecondaryViewController:ontoPrimaryViewController: method, the split view controller will shows only the content from its primary view controller.
But in my project, the split view controller shows both primary and secondary view controller in collapsed interface no matter I return true of false in this method. And the most wired thing is that this method is only called once when the app begins running.
Here is my custom SplitViewController which subclasses to UISplitViewController:
import UIKit
class SplitViewController: UISplitViewController, UISplitViewControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
}
// MARK:- UISplitViewControllerDelegate
func splitViewController(splitController: UISplitViewController, collapseSecondaryViewController secondaryViewController: UIViewController, ontoPrimaryViewController primaryViewController: UIViewController) -> Bool {
// We don't want anything to happen. Say we've dealt with it
return true
}
}
I found I needed to add "self.preferredDisplayMode=.primaryOverlay" to my ViewDidLoad.
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.delegate = self
self.preferredDisplayMode = .primaryOverlay
}
The preferredDisplayMode has some other options to customize the initial behavior you can toy with to get your preferred look and feel.
Note this is for iPhone, Compact Width. Test also on an iPad as it behaves differently (landscape vs. portrait).