iPad Landscape and Portrait different layouts with Size Class - ios

How to design iPad Landscape and Portrait screens with different Layouts using Size class.
I could find only w-regular and h-regular for both orientations. Example: I need to align 2 views vertically in portrait and horizontally in landscape using Size Class

Finally I found a solution :
if traitCollection.verticalSizeClass == .Regular && traitCollection.horizontalSizeClass == .Regular {
var orientation:UIInterfaceOrientation = UIApplication.sharedApplication().statusBarOrientation;
if orientation == UIInterfaceOrientation.LandscapeLeft || orientation == UIInterfaceOrientation.LandscapeRight {
// orientation is landscape
} else {
// orientation is portrait
}
}

It appears to be Apple's intent to treat both iPad orientations as the same -- but as a number of us are finding, there are very legitimate design reasons to want to vary the UI layout for iPad Portrait vs. iPad Landscape.
However, please see this answer for another approach to adapting size classes to do what we need:
https://stackoverflow.com/a/28268200/4517929

For Swift 3 it would look like this:
override func overrideTraitCollection(forChildViewController childViewController: UIViewController) -> UITraitCollection? {
if UI_USER_INTERFACE_IDIOM() == .pad &&
view.bounds.width > view.bounds.height {
let collections = [UITraitCollection(horizontalSizeClass: .regular),
UITraitCollection(verticalSizeClass: .compact)]
return UITraitCollection(traitsFrom: collections)
}
return super.overrideTraitCollection(forChildViewController: childViewController)
}
It will use wRhC instead of wRhR for iPad devices in landscape mode.
Put this code to your base view controller, i.e. this rule will work for all controllers that were presented by this one.
You can put any additional conditions here... For example, if you want this rule to be working only for specific view controller, your if operator would look like this:
if UI_USER_INTERFACE_IDIOM() == .pad &&
childViewController is YourSpecificViewController &&
view.bounds.width > view.bounds.height {
let collections = [UITraitCollection(horizontalSizeClass: .regular),
UITraitCollection(verticalSizeClass: .compact)]
return UITraitCollection(traitsFrom: collections)
}

Swift 4
override func overrideTraitCollection(forChildViewController childViewController: UIViewController) -> UITraitCollection? {
if UIDevice.current.userInterfaceIdiom == .pad && UIDevice.current.orientation.isLandscape {
return UITraitCollection(traitsFrom:[UITraitCollection(verticalSizeClass: .compact), UITraitCollection(horizontalSizeClass: .regular)])
}
return super.overrideTraitCollection(forChildViewController: childViewController)
}
I like to create a custom subclass of navigationController and then set a storyboards initial Navigation controller to that class. You can also do something similar with a ViewController.
Example:
import UIKit
class NavigationControllerWithTraitOverride: UINavigationController {
// If you make a navigationController a member of this class the descendentVCs of that navigationController will have their trait collection overridden with compact vertical size class if the user is on an iPad and the device is horizontal.
override func overrideTraitCollection(forChildViewController childViewController: UIViewController) -> UITraitCollection? {
if UIDevice.current.userInterfaceIdiom == .pad && UIDevice.current.orientation.isLandscape {
return UITraitCollection(traitsFrom:[UITraitCollection(verticalSizeClass: .compact), UITraitCollection(horizontalSizeClass: .regular)])
}
return super.overrideTraitCollection(forChildViewController: childViewController)
}
}
Note: You should not override traitCollection as per the docs
Important
Use the traitCollection property directly. Do not override it. Do not provide a custom implementation.

Related

Detecting orientation on loading

When my simulator is in portrait and when my viewcontroller loads initially, it prints out Landscape instead of Portrait but when I change the orientaiton, it correctly displays the orientation so forth. I did the following
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if (UIDevice.current.orientation == UIDeviceOrientation.portrait || UIDevice.current.orientation == UIDeviceOrientation.portraitUpsideDown)
{
print("Portrait")
}
else
{
print("Landscape")
}
}
I have no idea why it is displaying wrong orientation when it loads initially but once I rotate everything seems to work.
P.S. It seems like when simulator initially loads, the orientaiton is unknown, so it is choosing the else condition, how to avoid this from happening and identify the correct orientaiton?
You can check current orientation by UIApplication.shared.statusBarOrientation.isPortrait or UIApplication.shared.statusBarOrientation.isLandscape.
if UIDevice.currentDevice().orientation.isLandscape.boolValue {
print("landscape")
} else {
print("portrait")
}

UISplitViewController - prevent splitting in landscape on iPhone 6 plus

I am using a UISplitViewController in my app. This works just fine on iPad where primary and secondary are always visible, and it works just fine on most iPhones where it acts like a UINavigationController.
On iPhone 6+ and 6S+ the split view acts like an iPhone in portrait and like an iPad in landscape. This splitting in landscape is causing me problems and I'd like to avoid it.
Is there any way to prevent the UISplitViewController from showing primary and secondary controllers in iPhone 6+ landscape? I just want it to show the secondary controller, the same as it would do for other iPhones.
Thanks.
I was able to do this by subclassing the UISplitViewController and then overriding the trait collection to return a compact horizontal size class when the device is not an iPad. I know checking the interface idiom is a faux-pas these days, but I wasn't sure how else to do it.
I simply added this method to my UISplitViewController subclass:
-(UITraitCollection *)traitCollection {
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
return [super traitCollection];
} else {
return [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassCompact];
}
}
Any suggestions for a better way to do this are certainly welcome.
Here is the same answer in Swift but also with a fix where the vertical size class would be wrong on phone in landscape:
override var traitCollection: UITraitCollection {
if UI_USER_INTERFACE_IDIOM() == .pad {
return super.traitCollection
} else {
let horizontal = UITraitCollection(horizontalSizeClass: .compact)
let vertical = UITraitCollection(verticalSizeClass: super.traitCollection.verticalSizeClass)
return UITraitCollection.init(traitsFrom: [horizontal, vertical])
}
}
I had some issues with UINavigationControllers not displaying correctly with the code above. This is the method that worked for me (Swift 5):
1) Create a UIViewController containing a UIContainerView
2) Embed your UISplitViewController in that container
3) Add the following code:
class SplitViewContainerViewController: UIViewController {
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
if UI_USER_INTERFACE_IDIOM() != .pad {
performOverrideTraitCollection()
}
}
private func performOverrideTraitCollection() {
for childVC in self.children {
setOverrideTraitCollection(UITraitCollection(horizontalSizeClass: .compact), forChild: childVC)
}
}
}
4) Set the view controller containing the container view to be SplitViewContainerViewController
Update For iOS 13
The code above no longer works on iOS 13. Use the following instead on the SplitViewContainerViewController class:
override func overrideTraitCollection(forChild childViewController: UIViewController) -> UITraitCollection? {
if UIDevice.current.userInterfaceIdiom != .pad {
return UITraitCollection(horizontalSizeClass: .compact)
} else {
return super.traitCollection
}
}

Support landscape orientation for horizontalSizeClass=.Regular in iOS

I'm trying to build an app that supports portrait AND landscape orientations for iOS devices with regular horizontal size class, and portrait only for the rest.
At the time of this writing, it would be: Portrait only (for iPhones except 6 Plus/6s Plus) and Portrait AND Landscape for iPhone 6 Plus / 6s Plus, and iPad.
This is a similar behavior performed by the native Mail app.
I've tried among other things, the following:
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
return [.Portrait, .LandscapeLeft, .LandscapeRight]
}
override func shouldAutorotate() -> Bool {
return (traitCollection.horizontalSizeClass == .Regular)
}
However, shouldAutororate is obviously called before the interface rotates, so that happens before traitCollection gets updated.
So, the question is, how to achieve this? I'm trying to accomplish this in the cleanest way possible without referencing explicitly userInterfaceIdiom, screen size, etc.
override var supportedInterfaceOrientations:UIInterfaceOrientationMask{
return [.portrait, .landscapeLeft, .landscapeRight]
}
override var shouldAutorotate:Bool {
return (traitCollection.horizontalSizeClass == .regular) || (traitCollection.displayScale > 2);
}

Different size classes for iPad portrait and landscape modes with containerviews

I've found this question and I've tried to implement the solution that has been given. However I run into a problem.
My initial view controller has two container views who both have their own view controller. I've created a root view controller that is assigned to the initial view controller. The code in this class looks like this.
class RootViewController: UIViewController {
var willTransitionToPortrait: Bool!
var traitCollection_CompactRegular: UITraitCollection!
var traitCollection_AnyAny: UITraitCollection!
override func viewDidLoad() {
super.viewDidLoad()
setupReferenceSizeClasses()
// Do any additional setup after loading the view.
}
override func viewWillAppear(animated: Bool) {
willTransitionToPortrait = self.view.frame.size.height > self.view.frame.size.width
}
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
willTransitionToPortrait = size.height > size.width
}
func setupReferenceSizeClasses(){
let traitCollection_hCompact = UITraitCollection(horizontalSizeClass: .Compact)
let traitCollection_vRegular = UITraitCollection(verticalSizeClass: .Regular)
traitCollection_CompactRegular = UITraitCollection(traitsFromCollections: [traitCollection_hCompact, traitCollection_vRegular])
let traitCollection_hAny = UITraitCollection(horizontalSizeClass: .Unspecified)
let traitCollection_vAny = UITraitCollection(verticalSizeClass: .Unspecified)
traitCollection_AnyAny = UITraitCollection(traitsFromCollections: [traitCollection_hAny, traitCollection_vAny])
}
override func overrideTraitCollectionForChildViewController(childViewController: UIViewController) -> UITraitCollection? {
let traitCollectionForOverride = ((willTransitionToPortrait) != nil) ? traitCollection_CompactRegular : traitCollection_AnyAny
return traitCollectionForOverride;
}
However when I run it the size class won't respons like it should. One of the container view controllers will start acting weird in both landscape and portrait mode like can be seen below.
When I don't assign the rootviewcontroller it will look like this
While it should look like this in portrait mode
Does anyone know what might be going wrong here? Why it doesn't change the size class like desired.
EDIT
Like #Muhammad Yawar Ali asked here are screenshots from the position of all the size classes I've set. I have no warnings or errors on any constraints so these screenshots contain the updated views.
I hope this shows everything that is needed.
EDIT:
for some reason I'm unable to put in all the screenshots
On the viewWillTransitionToSize you need to call also super, to pass the event to the next responder (your rootviewcontroller)...
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
willTransitionToPortrait = size.height > size.width
}
Realize this is over two years old but...
I just ran across what I think is a similar issue. What you may be forgetting is that 'overrideTraitCollectionForChildViewController' only overrides the views children, so this method won't do anything with the containers since they are located at the root.
I solved this putting my two containers in a UIStackView in Interface Builder and made a property of this stack in code and then updated the axis depending on the orientation. For example, in Objective-C:
#property (weak, nonatomic) IBOutlet UIStackView *rootStack;
// ...
- (UITraitCollection *)overrideTraitCollectionForChildViewController:(UIViewController *)childViewController
{
if (UI_USER_INTERFACE_IDIOM() != UIUserInterfaceIdiomPad) {
return [super overrideTraitCollectionForChildViewController:childViewController];
}
if (CGRectGetWidth(self.view.bounds) < CGRectGetHeight(self.view.bounds)) {
self.rootStack.axis = UILayoutConstraintAxisVertical;
return [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassCompact];
}
else {
self.rootStack.axis = UILayoutConstraintAxisHorizontal;
return [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassRegular];
}
If you have any constraints that are different between portrait and landscape you will need to adjust those in code as well.
I suppose you could also solve this by embedding the view controller with the containers in another view controller.
I have cloned your code from repository : https://github.com/MaikoHermans/sizeClasses.git
And editted code put the below code in you controller it will work fine & will not effect your design in iPads.
import UIKit
class RootViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func overrideTraitCollectionForChildViewController(childViewController: UIViewController) -> UITraitCollection? {
if view.bounds.width < view.bounds.height {
return UITraitCollection(horizontalSizeClass: .Unspecified)
} else {
return UITraitCollection(horizontalSizeClass: .Regular)
}
}
}
You can try with this code but There is an issue i believe its not updating traits properly for ipads and view layout remains same but looks good. I have tried multiple ways but not succeeded yet will update my answer.

How to determine the Today extension left margin properly in iOS 8?

I'm trying to find out how to calculate the left margin in the Today extension main view to align the contents to the rest of the Today view labels.
Here's an example with a clean Xcode project using a Today extensions (I've added color to the view backgrounds and drawn a dashed red line to illustrate where I'd like to align the Hello World UILabel).
The result in iPhone 6 Plus simulator (left side landscape, right side portrait) can be found from the image below:
In the image, notice that the green main view left boundary is placed differently related to the app name UILabel "testi2". It also seems that the red line - main views left border alignment is different in each device: iPhone 5x, iPhone 6 and iPads.
The behavior can be reproduced using a clean Xcode project (I'm using Xcode 6.1.1, iOS 8.1 and Swift):
Create an empty Xcode project (A single-view application)
Add a new Target: Extensions > Today extension
From the Today extension group, find MainInterface.storyboard and make the main view background green and Hello world UILabel background red:
How do I align the the Hello World UILabel (red background) to the dashed line? Or how do I align the main view (green background) to the dashed line?
Did you try this one ?
Objective-C:
- (UIEdgeInsets)widgetMarginInsetsForProposedMarginInsets:(UIEdgeInsets)defaultMarginInsets
{
return UIEdgeInsetsZero;
}
Swift:
func widgetMarginInsetsForProposedMarginInsets(defaultMarginInsets: UIEdgeInsets) -> UIEdgeInsets {
return UIEdgeInsetsZero
}
Otherwise it looks like you will have to set them manually according to this So-Thread:
EDIT:
Looks like this method is deprecated and is not called for devices running >= iOS10. However I could not find any documentation on an alternative. If you have any information on this please add to this post so everyone can profit. Just make sure when using this function that it will not be called on >= iOS10.
Source: Apple Documentation
I tried using the left value of the defaultMarginInset of -widgetMarginInsetsForProposedMarginInsets with mixed results: On the iPhone 5S screen dimensions, it can be used to get the same inset as the default calendar widget of iOS (note that the right margin of the time label aligns with the blue line):
On the iPhone 6, you get similar results, i.e. it also aligns. However, on iPhone 6 Plus, the calendar widget somehow scales the inset:
Note that in the landscape version, neither the time nor the lines align to anything.
In conclusion, I would say that you can safely use defaultMarginInset.left to get decent results.
Swift code:
class TodayViewController: UIViewController, NCWidgetProviding {
var defaultLeftInset: CGFloat = 0
var marginIndicator = UIView()
override func viewDidLoad() {
super.viewDidLoad()
marginIndicator.backgroundColor = UIColor.whiteColor()
view.addSubview(marginIndicator)
}
func widgetMarginInsetsForProposedMarginInsets(var defaultMarginInsets: UIEdgeInsets) -> UIEdgeInsets {
defaultLeftInset = defaultMarginInsets.left
defaultMarginInsets.left = 0
return defaultMarginInsets
}
func widgetPerformUpdateWithCompletionHandler(completionHandler: ((NCUpdateResult) -> Void)!) {
marginIndicator.frame = CGRectMake(defaultLeftInset, 0, 10, view.frame.size.height)
completionHandler(NCUpdateResult.NewData)
}
}
My temporary solution goes as follows. I'm using constant values for setting the left and top margins. This way I can align the content exactly like it's, for example, in Tomorrow Summary widget.
First some helper methods for determining the device type (adapted from this answer):
struct ScreenSize {
static let SCREEN_WIDTH = UIScreen.mainScreen().bounds.size.width
static let SCREEN_HEIGHT = UIScreen.mainScreen().bounds.size.height
static let SCREEN_MAX_LENGTH = max(ScreenSize.SCREEN_WIDTH,
ScreenSize.SCREEN_HEIGHT)
static let SCREEN_MIN_LENGTH = min(ScreenSize.SCREEN_WIDTH,
ScreenSize.SCREEN_HEIGHT)
}
struct DeviceType {
static let iPhone4 = UIDevice.currentDevice().userInterfaceIdiom == .Phone
&& ScreenSize.SCREEN_MAX_LENGTH < 568.0
static let iPhone5 = UIDevice.currentDevice().userInterfaceIdiom == .Phone
&& ScreenSize.SCREEN_MAX_LENGTH == 568.0
static let iPhone6 = UIDevice.currentDevice().userInterfaceIdiom == .Phone
&& ScreenSize.SCREEN_MAX_LENGTH == 667.0
static let iPhone6Plus = UIDevice.currentDevice().userInterfaceIdiom == .Phone
&& ScreenSize.SCREEN_MAX_LENGTH == 736.0
static let iPad = UIDevice.currentDevice().userInterfaceIdiom == .Pad
}
Then using widgetMarginInsetsForProposedMarginInsets: as suggested I overwrite the left and top insets as follows:
func widgetMarginInsetsForProposedMarginInsets(defaultMarginInsets: UIEdgeInsets)
-> UIEdgeInsets {
var insets = defaultMarginInsets
let isPortrait = UIScreen.mainScreen().bounds.size.width
< UIScreen.mainScreen().bounds.size.height
insets.top = 10.0
if DeviceType.iPhone6Plus {
insets.left = isPortrait ? 53.0 : 82.0
} else if DeviceType.iPhone6 {
insets.left = 49.0
} else if DeviceType.iPhone5 {
insets.left = 49.0
} else if DeviceType.iPhone4 {
insets.left = 49.0
} else if DeviceType.iPad {
insets.left = isPortrait ? 58.0 : 58.0
}
return insets
}
However, this is not the solution I'm looking for - I'd like to get rid of hardcoding per-device-per-orientation pixel values.

Resources