I basically want to have my subviews positioned differently depending upon the orientation of the iPad (Portrait or Landscape) using Sizing Classes introduced in xcode 6. I have found numerous tutorials explaining how different sizing classes are available for Iphones in portrait and landscape on the IB but however there seem to be none that cover individual landscape or portrait modes for the iPad on IB. Can anyone help?
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.
Unfortunately, the current OS doesn't seem to provide support for this distinction ... meaning that we're back to manipulating auto-layout constraints in code or similar workarounds to achieve what we should ideally be able to get for free using Adaptive UI.
Not an elegant solution.
Isn't there a way to leverage the magic that Apple's already built into IB and UIKit to use a size class of our choosing for a given orientation?
~
In thinking about the problem more generically, I realized that 'size classes' are simply ways to address multiple layouts that are stored in IB, so that they can be called up as needed at runtime.
In fact, a 'size class' is really just a pair of enum values. From UIInterface.h:
typedef NS_ENUM(NSInteger, UIUserInterfaceSizeClass) {
UIUserInterfaceSizeClassUnspecified = 0,
UIUserInterfaceSizeClassCompact = 1,
UIUserInterfaceSizeClassRegular = 2,
} NS_ENUM_AVAILABLE_IOS(8_0);
So regardless of what Apple has decided to name these different variations, fundamentally, they're just a pair of integers used as a unique identifier of sorts, to distinguish one layout from another, stored in IB.
Now, supposing that we create an alternate layout (using a unused size class) in IB -- say, for iPad Portrait ... is there a way to have the device use our choice of size class (UI layout) as needed at runtime?
After trying several different (less elegant) approaches to the problem, I suspected there might be a way to override the default size class programmatically. And there is (in UIViewController.h):
// Call to modify the trait collection for child view controllers.
- (void)setOverrideTraitCollection:(UITraitCollection *)collection forChildViewController:(UIViewController *)childViewController NS_AVAILABLE_IOS(8_0);
- (UITraitCollection *)overrideTraitCollectionForChildViewController:(UIViewController *)childViewController NS_AVAILABLE_IOS(8_0);
Thus, if you can package your view controller hierarchy as a 'child' view controller, and add it to a top-level parent view controller ... then you can conditionally override the child into thinking that it's a different size class than the default from the OS.
Here's a sample implementation that does this, in the 'parent' view controller:
#interface RDTraitCollectionOverrideViewController : UIViewController {
BOOL _willTransitionToPortrait;
UITraitCollection *_traitCollection_CompactRegular;
UITraitCollection *_traitCollection_AnyAny;
}
#end
#implementation RDTraitCollectionOverrideViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self setUpReferenceSizeClasses];
}
- (void)setUpReferenceSizeClasses {
UITraitCollection *traitCollection_hCompact = [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassCompact];
UITraitCollection *traitCollection_vRegular = [UITraitCollection traitCollectionWithVerticalSizeClass:UIUserInterfaceSizeClassRegular];
_traitCollection_CompactRegular = [UITraitCollection traitCollectionWithTraitsFromCollections:#[traitCollection_hCompact, traitCollection_vRegular]];
UITraitCollection *traitCollection_hAny = [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassUnspecified];
UITraitCollection *traitCollection_vAny = [UITraitCollection traitCollectionWithVerticalSizeClass:UIUserInterfaceSizeClassUnspecified];
_traitCollection_AnyAny = [UITraitCollection traitCollectionWithTraitsFromCollections:#[traitCollection_hAny, traitCollection_vAny]];
}
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
_willTransitionToPortrait = self.view.frame.size.height > self.view.frame.size.width;
}
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]
_willTransitionToPortrait = size.height > size.width;
}
-(UITraitCollection *)overrideTraitCollectionForChildViewController:(UIViewController *)childViewController {
UITraitCollection *traitCollectionForOverride = _willTransitionToPortrait ? _traitCollection_CompactRegular : _traitCollection_AnyAny;
return traitCollectionForOverride;
}
#end
As a quick demo to see whether it worked, I added custom labels specifically to the 'Regular/Regular' and 'Compact/Regular' versions of the child controller layout in IB:
And here's what it looks like running, when the iPad is in both orientations:
Voila! Custom size class configurations at runtime.
Hopefully Apple will make this unnecessary in the next version of the OS. In the meantime, this may be a more elegant and scalable approach than programmatically messing with auto-layout constraints or doing other manipulations in code.
~
EDIT (6/4/15): Please bear in mind that the sample code above is essentially a proof of concept to demonstrate the technique. Feel free to adapt as needed for your own specific application.
~
EDIT (7/24/15): It's gratifying that the above explanation seems to help demystify the issue. While I haven't tested it, the code by mohamede1945 [below] looks like a helpful optimization for practical purposes. Feel free to test it out and let us know what you think. (In the interest of completeness, I'll leave the sample code above as-is.)
As a summary to the very long answer by RonDiamond. All you need to do is in your root view controller.
Objective-c
- (UITraitCollection *)overrideTraitCollectionForChildViewController:(UIViewController *)childViewController
{
if (CGRectGetWidth(self.view.bounds) < CGRectGetHeight(self.view.bounds)) {
return [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassCompact];
} else {
return [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassRegular];
}
}
Swift:
override func overrideTraitCollectionForChildViewController(childViewController: UIViewController) -> UITraitCollection! {
if view.bounds.width < view.bounds.height {
return UITraitCollection(horizontalSizeClass: .Compact)
} else {
return UITraitCollection(horizontalSizeClass: .Regular)
}
}
Then in storyborad use compact width for Portrait and Regular width for Landscape.
The long and helpful answer by RonDiamond is a good start to comprehend the principles, however the code that worked for me (iOS 8+) is based on overriding method (UITraitCollection *)traitCollection
So, add constraints in InterfaceBuilder with variations for Width - Compact, for example for constraint's property Installed. So Width - Any will be valid for landscape, Width - Compact for Portrait.
To switch constraints in the code based on current view controller size, just add the following into your UIViewController class:
- (UITraitCollection *)traitCollection
{
UITraitCollection *verticalRegular = [UITraitCollection traitCollectionWithVerticalSizeClass:UIUserInterfaceSizeClassRegular];
if (self.view.bounds.size.width < self.view.bounds.size.height) {
// wCompact, hRegular
return [UITraitCollection traitCollectionWithTraitsFromCollections:
#[[UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassCompact],
verticalRegular]];
} else {
// wRegular, hRegular
return [UITraitCollection traitCollectionWithTraitsFromCollections:
#[[UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassRegular],
verticalRegular]];
}
}
The iPad has the 'regular' size trait for both horizontal and vertical dimensions, giving no distinction between portrait and landscape.
These size traits can be overridden in your custom UIViewController subclass code, via method traitCollection, for example:
- (UITraitCollection *)traitCollection {
// Distinguish portrait and landscape size traits for iPad, similar to iPhone 7 Plus.
// Be aware that `traitCollection` documentation advises against overriding it.
UITraitCollection *superTraits = [super traitCollection];
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
UITraitCollection *horizontalRegular = [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassRegular];
UITraitCollection *verticalRegular = [UITraitCollection traitCollectionWithVerticalSizeClass:UIUserInterfaceSizeClassRegular];
UITraitCollection *regular = [UITraitCollection traitCollectionWithTraitsFromCollections:#[horizontalRegular, verticalRegular]];
if ([superTraits containsTraitsInCollection:regular]) {
if (UIInterfaceOrientationIsPortrait([[UIApplication sharedApplication] statusBarOrientation])) {
// iPad in portrait orientation
UITraitCollection *horizontalCompact = [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassCompact];
return [UITraitCollection traitCollectionWithTraitsFromCollections:#[superTraits, horizontalCompact, verticalRegular]];
} else {
// iPad in landscape orientation
UITraitCollection *verticalCompact = [UITraitCollection traitCollectionWithVerticalSizeClass:UIUserInterfaceSizeClassCompact];
return [UITraitCollection traitCollectionWithTraitsFromCollections:#[superTraits, horizontalRegular, verticalCompact]];
}
}
}
return superTraits;
}
- (BOOL)prefersStatusBarHidden {
// Override to negate this documented special case, and avoid erratic hiding of status bar in conjunction with `traitCollection` override:
// For apps linked against iOS 8 or later, this method returns true if the view controller is in a vertically compact environment.
return NO;
}
This gives the iPad the same size traits as the iPhone 7 Plus. Note that other iPhone models generally have the 'compact width' trait (rather than regular width) regardless of orientation.
Mimicking the iPhone 7 Plus in this way allows that model to be used as a stand-in for the iPad in Xcode's Interface Builder, which is unaware of customizations in code.
Be aware that Split View on the iPad may use different size traits from normal full screen operation.
This answer is based on the approach taken in this blog post, with some improvements.
Update 2019-01-02: Updated to fix intermittent hidden status bar in iPad landscape, and potential trampling of (newer) traits in UITraitCollection. Also noted that Apple documentation actually recommends against overriding traitCollection, so in future there may turn out to be issues with this technique.
How much different is your landscape mode going to be than your portrait mode? If its very different, it might be a good idea to create another view controller and load it when the device is in landscape
For example
if (UIDeviceOrientationIsLandscape([UIDevice currentDevice].orientation))
//load landscape view controller here
Swift 5 Version. It works fine.
override func overrideTraitCollection(forChild childViewController: UIViewController) -> UITraitCollection? {
if UIScreen.main.bounds.width > UIScreen.main.bounds.height {
let collections = [UITraitCollection(horizontalSizeClass: .regular),
UITraitCollection(verticalSizeClass: .compact)]
return UITraitCollection(traitsFrom: collections)
}
return super.overrideTraitCollection(forChild: childViewController)
}
Swift 3.0 code for #RonDiamond solution
class Test : UIViewController {
var _willTransitionToPortrait: Bool?
var _traitCollection_CompactRegular: UITraitCollection?
var _traitCollection_AnyAny: UITraitCollection?
func viewDidLoad() {
super.viewDidLoad()
self.upReferenceSizeClasses = null
}
func setUpReferenceSizeClasses() {
var traitCollection_hCompact: UITraitCollection = UITraitCollection(horizontalSizeClass: UIUserInterfaceSizeClassCompact)
var traitCollection_vRegular: UITraitCollection = UITraitCollection(verticalSizeClass: UIUserInterfaceSizeClassRegular)
_traitCollection_CompactRegular = UITraitCollection(traitsFromCollections: [traitCollection_hCompact,traitCollection_vRegular])
var traitCollection_hAny: UITraitCollection = UITraitCollection(horizontalSizeClass: UIUserInterfaceSizeClassUnspecified)
var traitCollection_vAny: UITraitCollection = UITraitCollection(verticalSizeClass: UIUserInterfaceSizeClassUnspecified)
_traitCollection_AnyAny = UITraitCollection(traitsFromCollections: [traitCollection_hAny,traitCollection_vAny])
}
func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
_willTransitionToPortrait = self.view.frame.size.height > self.view.frame.size.width
}
func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
_willTransitionToPortrait = size.height > size.width
}
func overrideTraitCollectionForChildViewController(childViewController: UIViewController) -> UITraitCollection {
var traitCollectionForOverride: UITraitCollection = _willTransitionToPortrait ? _traitCollection_CompactRegular : _traitCollection_AnyAny
return traitCollectionForOverride
}}
Related
I have a universal application running both on iPads and iPhones. The application starts with a .xib file, built in interface builder, which acts as the launch image. Once the app launched, it switches to the appropriate view controller based on device size set in the app delegate:
CGSize screenSize = [[UIScreen mainScreen] bounds].size;
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
if (screenSize.height <= 568.0f) {
// iPhone 4, 4S, 5, 5C, 5S, SE
self.viewController = [[iPhoneSmallViewController alloc] init];
} else {
// All other iPhones
self.viewController = [[iPhoneLargeViewController alloc] init];
}
} else {
// All iPad models
self.viewController = [[iPadViewController alloc] init];
}
The iPad view controller supports all interface orientations (set in app targets/main setup page), but on iPhones I only allow portrait mode restricted in the view controller as such:
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
{
return (UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskPortraitUpsideDown);
}
I have two problems with this method:
If the iPhone is held horizontally, the app still loads in portrait mode (as per the restrictions, which is all good) but all measurements are in landscape upon initialization. UI elements stick out on the side because they were measured for a landscape view but placed on a portrait.
I use the window's size to set up everything inside the view by initializing the following variable in the ViewDidLoad method:
windowSize = [[UIScreen mainScreen] bounds].size;
Tt gives me landscape dimensions in phone is held horizontally, even though landscape mode is not allowed.
If the app loads with landscape measurements initially, all my sorting of screen sizes in the app delegate are off since I identify iPhone models by measuring screen width that is only good in portrait mode.
Question: does anyone have a way to handle this complex problem in an elegant and simple way?
Some additional info: I use Xcode 10, support all the way back to iOS9 and do everything programmatically in Objective C.
p.s: I think this method worked before but not any more in iOS 12. But I could be wrong...
Edit: I provide an image of what I want to accomplish, and all help would be greatly appreciated. As I said, this has worked before (the app is quite old), but in recent iOS releases got increasingly buggy and desperately needs a cleanup, which is what I need help with.
One thing that might solve my problem, is if I could somehow restrict interface orientations based on device type in the launchScreen.xib, as I believe that is what causes the faulty behavior on iPhones.
Maybe this SO will be helpful,
They are detecting the device orientation and then rotating the view, look at first answer:
Change orientation programmatically with button - iOS
UIInterfaceOrientation currentOrientation = [UIApplication sharedApplication].statusBarOrientation;
NSNumber *value = [NSNumber numberWithInt:UIInterfaceOrientationPortrait];
[[UIDevice currentDevice] setValue:value forKey:#"orientation"];
[UIViewController attemptRotationToDeviceOrientation];
A better way to detect device is explained here:
iOS detect if user is on an iPad
if ( UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad )
{
return YES; /* Device is iPad */
}
I've been experimenting with this for days, and worked out a solution. Although this is probably not the most elegant way to do it, so if anyone has a better solution, please feel free to post it.
It is important to allow all interface orientations in the info.plist because I was unable to restrict them based on device size in the launchScreen.xib.
Create the universal launch screen that supports both iPads and iPhones. Because all interface orientations are allowed in the info.plist, this will have no restrictions.
Below is my current method in the app delegate. This is not the best way to do identifying the smaller iPhones (which I need for reasons... :), but because of the size differences, this works quite well.
At this point, the phone can be in any of the four interface orientations set in the info.plist, but because only the small handsets have a 320-width it is easy to catch it:
// Get screen size
CGSize screenSize = [[UIScreen mainScreen] bounds].size;
// Determine device based on screen dimensions
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
if ( (screenSize.height == 320.0f) || (screenSize.width == 320.0f) ){
// iPhone 4, 4S, 5, 5C, 5S, SE
self.viewController = [[iPhoneSmallViewController alloc] init];
} else {
// iPhone 6, 6S, 6P, 6SP, 7, 7P, 8, 8P X, XS, XM, XR
self.viewController = [[iPhoneLargeViewController alloc] init]; //Same as previous
}
} else {
// All iPad models
self.viewController = [[iPadViewController alloc] init];
}
Restrict interface orientations in the iPhone view controllers (all other view controllers will inherit the ones we set in the info.plist).
Do it like so:
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
return (UIInterfaceOrientationMaskPortrait);
}
There is another trick we need to do for the above to work though. When the window loads for the first time, it will not take into consideration the restriction we added to the view controller. That means, if we do our setup in the viewDidLoad method, we will receive landscape screen dimensions if the device is held horizontally (even though this orientation is not allowed). Restriction will be applied once the viewDidLoad method has concluded.
Therefore, to prevent buggy behavior, you need to create a separate method in which you do your setup (such as postViewDidLoad) and call it once the real viewDidLoad had concluded:
- (void) viewDidLoad
{
[super viewDidLoad];
[self performSelector:#selector(postViewDidLoad) withObject:nil afterDelay:0.0f];
}
In this method, you will get access the real screen size based on your restrictions you set in the supportedInterfaceOrientations method.
And that is basically it. If you have multiple views, all with different restrictions, just follow steps 4 and 5 in each of them to properly setup your workflow.
It seems in macOS 10.14 Mojave, the only way to create NSImage instances that automatically draw a light and dark version is via asset catalogs and +[NSImage imageNamed:]. However, I need to create dynamic images at runtime and there doesn't seem to be a way to do so without using private API.
Under the hood, it seems a private property _appearanceName has been introduced to NSImageRep that is somehow used to select the correct representation. It should be straight forward to create an NSImage with image representations that have the corresponding _appearanceName set but I would like to avoid this.
I found a simple workaround (posted below) but it doesn't seem to work correctly when the system appearance is changing (i.e. user is switching from light to dark or vice versa) or when used in view hierarchies that have the appearance property set to different appearances (e.g. one view hard-coded to dark mode, another view hard-coded to light mode).
So, how can I manually create a dynamic NSImage that is correctly showing a light or dark version, like the asset catalog images do?
#implementation NSImage (CustomDynamic)
+ (NSImage *)imageWithLight:(NSImage *)light dark:(NSImage *)dark
{
if (#available(macOS 10.14, *)) {
return [NSImage
imageWithSize:light.size
flipped:NO
drawingHandler:^(NSRect dstRect) {
if ([NSImage appearanceIsDarkMode:NSAppearance.currentAppearance]) {
[dark drawInRect:dstRect];
} else {
[light drawInRect:dstRect];
}
return YES;
}
];
} else {
return light;
}
}
+ (BOOL)appearanceIsDarkMode:(NSAppearance *)appearance
{
if (#available(macOS 10.14, *)) {
NSAppearanceName basicAppearance = [appearance bestMatchFromAppearancesWithNames:#[
NSAppearanceNameAqua,
NSAppearanceNameDarkAqua
]];
return [basicAppearance isEqualToString:NSAppearanceNameDarkAqua];
} else {
return NO;
}
}
#end
D'uh, it turned out the code posted in the question works just fine! The drawing handler is in fact called at appropriate times and does handle all the appearance situations.
However, I had code that scaled and cached those images and it was still using the ancient [image lockFocus]; … [image unlockFocus]; way of drawing images instead of using +[NSImage imageWithSize:flipped:drawingHandler:].
I have a NSCollectionViewFlowLayout which contains the following:
- (NSSize) itemSize
{
return CGSizeMake(self.collectionView.bounds.size.width, kItemHeight);
} // End of itemSize
- (CGFloat) minimumLineSpacing
{
return 0;
} // End of minimumLineSpacing
- (CGFloat) minimumInteritemSpacing
{
return 0;
} // End of minimumInteritemSpacing
I've tried to ways to make the layout responsive (set the width whenever resized). I've tried adding the following:
[[NSNotificationCenter defaultCenter] addObserver: self
selector: #selector(onWindowDidResize:)
name: NSWindowDidResizeNotification
object: nil];
- (void) onWindowDidResize: (NSNotification*) notification
{
[connectionsListView.collectionViewLayout invalidateLayout];
} // End of windowDidResize:
And this works fine if I expand the collection view (resize larger). But if I attempt to collapse the view (resize smaller), I get the following exceptions:
The behavior of the UICollectionViewFlowLayout is not defined because:
The item width must be less than the width of the UICollectionView minus the section insets left and right values, minus the content insets left and right values.
The relevant UICollectionViewFlowLayout instance is <TestListLayout: 0x106f70f90>, and it is attached to <NSCollectionView: 0x106f76480>.
Any suggestions on how I can resolve this?
NOTE1: This is macOS and not iOS (even though the error message states UICollectionViewFlowLayout).
NOTE2: Even though I receive the warning/error the layout width works, but I would like to figure out the underlying issue.
I had the same problem but in my case I resized view. #Giles solution didn't work for me until I changed invalidation context
class MyCollectionViewFlowLayout: NSCollectionViewFlowLayout {
override func shouldInvalidateLayout(forBoundsChange newBounds: NSRect) -> Bool {
return true
}
override func invalidationContext(forBoundsChange newBounds: NSRect) -> NSCollectionViewLayoutInvalidationContext {
let context = super.invalidationContext(forBoundsChange: newBounds) as! NSCollectionViewFlowLayoutInvalidationContext
context.invalidateFlowLayoutDelegateMetrics = true
return context
}
}
Hope it helps someone as it took me couple of evenings to find solution
This is because you are using the didResize event, which is too late. Your items are too wide at the moment the window starts to shrink. Try using:
func shouldInvalidateLayout(forBoundsChange newBounds: NSRect) -> Bool {
return true
}
...when overriding flow layout, to get everything recalculated.
The source code I posted in the question works fine as of macOS 10.14 with no issues. I added the following to my window which displays the collection view.
// Only Mojave and after is resizable. Before that, a full sized collection view caused issues as listed
// at https://stackoverflow.com/questions/48567326/full-width-nscollectionviewflowlayout-with-nscollectionview
if(#available(macOS 10.14, *))
{
self.window.styleMask |= NSWindowStyleMaskResizable;
} // End of macOS 10.14+
In my iOS 8 app, this popover segue appears correctly on all devices in all orientations except for iPhone 6 Plus landscape:
This is how it looks on iPhone 6 Plus landscape (it is stretching almost from top to bottom):
And when it displays like this, clicking outside of the view doesn't dismiss it (although Cancel does work). Rotating back to portrait gets it back to normal.
All of the constraints in this UIViewController are installed on all size classes.
When debugging values in viewDidAppear: I see the following:
po self.view: frame = (0 0; 250 394)
po self.preferredContentSize (width = 250, height = 160)
What is causing the view's height to jump to 394?
I'm actually having the same issue with another popover segue in iPhone 6 Plus landscape as well. (And in case there was curiosity, I'm using a VC instead of 'UIAlertController' here because of the validation requirements of the UITextField displayed don't work well with UIAlertController.)
Edit to include my popover code:
This code is found in prepareForSegue:
FavoriteNameViewController *nameVC = segue.destinationViewController;
UIPopoverPresentationController *popPC = nameVC.popoverPresentationController;
popPC.delegate = self;
nameVC.delegate = self;
nameVC.view.center = self.originalContentView.center;
And then the delegate method:
- (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller {
return UIModalPresentationNone;
}
And here is the segue definition in Xcode:
What you're seeing is not a popover. It's a normal presented view. By default, a popover appears as a popover on iPad, but as a presented view on iPhone — including the iPhone 6 plus. On other iPhones, this presented view is fullscreen - it covers everything. But the iPhone 6 is so wide that they don't do that, so it appears in the middle of the screen at a standard width (the width of a smaller iPhone).
Thus, the preferred content size has no effect. This isn't a popover. Presented view controller views are given a standard size, and this one is no exception.
However, you can have the popover appear as a popover on iPhone. To do so:
Set a delegate for the popover view controller's popoverPresentationController before presenting it.
In the delegate, implement adaptivePresentationStyleForPresentationController: and return .None.
However, this is apparently not working on iPhone 6 Plus in landscape mode; the popover is not "adapting". I would describe this as a bug!
EDIT In iOS 9, the problem is solved! Implement the new delegate method adaptivePresentationStyleForPresentationController:traitCollection: to return .None and you'll get a popover under all circumstances, including the iPhone 6 Plus in landscape. Here's a complete working example where the popover is created and summoned in code in response to a button tap:
#IBAction func doButton(sender: AnyObject) {
let vc = MyViewController()
vc.preferredContentSize = CGSizeMake(400,500)
vc.modalPresentationStyle = .Popover
if let pres = vc.presentationController {
pres.delegate = self
}
self.presentViewController(vc, animated: true, completion: nil)
if let pop = vc.popoverPresentationController {
pop.sourceView = (sender as! UIView)
pop.sourceRect = (sender as! UIView).bounds
}
}
func adaptivePresentationStyleForPresentationController(
controller: UIPresentationController,
traitCollection: UITraitCollection)
-> UIModalPresentationStyle {
return .None
}
This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
How to develop or migrate apps for iPhone 5 screen resolution?
I was just wondering with how should we deal with the iPhone 5 bigger screen size.
As it has more pixels in height, things like GCRectMake that use coordinates (and just doubled the pixels with the retina/non retina problem) won't work seamlessly between versions, as it happened when we got the Retina.
And will we have to design two storyboards, just like for the iPad?
I personally don't think Apple will require you to check the screen size every time you have to draw something, like many answers say. Does that happen with the iPad?
All apps will continue to work in the vertically stretched screen from what I could tell in today's presentation. They will be letterboxed or basically the extra 88 points in height would simply be black.
If you only plan to support iOS 6+, then definitely consider using Auto Layout. It removes all fixed layout handling and instead uses constraints to lay things out. Nothing will be hard-coded, and your life will become a lot simpler.
However, if you have to support older iOS's, then it really depends on your application. A majority of applications that use a standard navigation bar, and/or tab bar, could simply expand the content in the middle to use up that extra points. Set the autoresizing mask of the center content to expand in both directions.
view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
It works great out of the box for table views, however, if your app used pixel-perfect layout for displaying content, then your best bet would be to re-imagine the content so that it can accommodate varying heights.
If that's not a possibility, then the only remaining option is to have two UIs (pre iPhone 5, and iPhone 5).
If that sounds ugly, then you could go with the default letterboxed model where the extra points/pixels just show up black.
Edit
To enable your apps to work with iPhone 5, you need to add a retina version of the launcher image. It should be named Default-568h#2x.png. And it has to be retina quality - there's no backward compatibility here :)
You could also select this image from within Xcode. Go to the target, and under the Summary section, look for Launch Images. The image has to be 640x1136 pixels in size. Here's a screenshot of where to find it, if that helps.
You need to add a 640x1136 pixels PNG image (Default-568h#2x.png) as a 4 inch default splash image of your project, and it will use extra spaces (without efforts on simple table based applications, games will require more efforts).
I've created a small UIDevice category in order to deal with all screen resolutions. You can get it here, but the code is as follows:
File UIDevice+Resolutions.h:
enum {
UIDeviceResolution_Unknown = 0,
UIDeviceResolution_iPhoneStandard = 1, // iPhone 1,3,3GS Standard Display (320x480px)
UIDeviceResolution_iPhoneRetina4 = 2, // iPhone 4,4S Retina Display 3.5" (640x960px)
UIDeviceResolution_iPhoneRetina5 = 3, // iPhone 5 Retina Display 4" (640x1136px)
UIDeviceResolution_iPadStandard = 4, // iPad 1,2,mini Standard Display (1024x768px)
UIDeviceResolution_iPadRetina = 5 // iPad 3 Retina Display (2048x1536px)
}; typedef NSUInteger UIDeviceResolution;
#interface UIDevice (Resolutions)
- (UIDeviceResolution)resolution;
NSString *NSStringFromResolution(UIDeviceResolution resolution);
#end
File UIDevice+Resolutions.m:
#import "UIDevice+Resolutions.h"
#implementation UIDevice (Resolutions)
- (UIDeviceResolution)resolution
{
UIDeviceResolution resolution = UIDeviceResolution_Unknown;
UIScreen *mainScreen = [UIScreen mainScreen];
CGFloat scale = ([mainScreen respondsToSelector:#selector(scale)] ? mainScreen.scale : 1.0f);
CGFloat pixelHeight = (CGRectGetHeight(mainScreen.bounds) * scale);
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone){
if (scale == 2.0f) {
if (pixelHeight == 960.0f)
resolution = UIDeviceResolution_iPhoneRetina4;
else if (pixelHeight == 1136.0f)
resolution = UIDeviceResolution_iPhoneRetina5;
} else if (scale == 1.0f && pixelHeight == 480.0f)
resolution = UIDeviceResolution_iPhoneStandard;
} else {
if (scale == 2.0f && pixelHeight == 2048.0f) {
resolution = UIDeviceResolution_iPadRetina;
} else if (scale == 1.0f && pixelHeight == 1024.0f) {
resolution = UIDeviceResolution_iPadStandard;
}
}
return resolution;
}
#end
This is how you need to use this code.
1) Add the above UIDevice+Resolutions.h & UIDevice+Resolutions.m files to your project
2) Add the line #import "UIDevice+Resolutions.h" to your ViewController.m
3) Add this code to check what versions of device you are dealing with
int valueDevice = [[UIDevice currentDevice] resolution];
NSLog(#"valueDevice: %d ...", valueDevice);
if (valueDevice == 0)
{
//unknow device - you got me!
}
else if (valueDevice == 1)
{
//standard iphone 3GS and lower
}
else if (valueDevice == 2)
{
//iphone 4 & 4S
}
else if (valueDevice == 3)
{
//iphone 5
}
else if (valueDevice == 4)
{
//ipad 2
}
else if (valueDevice == 5)
{
//ipad 3 - retina display
}
I have just finished updating and sending an iOS 6.0 version of one of my Apps to the store. This version is backwards compatible with iOS 5.0, thus I kept the shouldAutorotateToInterfaceOrientation: method and added the new ones as listed below.
I had to do the following:
Autorotation is changing in iOS 6. In iOS 6, the shouldAutorotateToInterfaceOrientation: method of UIViewController is deprecated. In its place, you should use the supportedInterfaceOrientationsForWindow: and shouldAutorotate methods.
Thus, I added these new methods (and kept the old for iOS 5 compatibility):
- (BOOL)shouldAutorotate {
return YES;
}
- (NSUInteger)supportedInterfaceOrientations {
return UIInterfaceOrientationMaskAllButUpsideDown;
}
Used the view controller’s viewWillLayoutSubviews method and adjust the layout using the view’s bounds rectangle.
Modal view controllers: The willRotateToInterfaceOrientation:duration:,
willAnimateRotationToInterfaceOrientation:duration:, and
didRotateFromInterfaceOrientation: methods are no longer called on
any view controller that makes a full-screen presentation over
itself—for example, presentViewController:animated:completion:.
Then I fixed the autolayout for views that needed it.
Copied images from the simulator for startup view and views for the iTunes store into PhotoShop and exported them as png files.
The name of the default image is: Default-568h#2x.png and the size is 640×1136. It´s also allowed to supply 640×1096 for the same portrait mode (Statusbar removed). Similar sizes may also be supplied in landscape mode if your app only allows landscape orientation on the iPhone.
I have dropped backward compatibility for iOS 4. The main reason for that is because support for armv6 code has been dropped. Thus, all devices that I am able to support now (running armv7) can be upgraded to iOS 5.
I am also generation armv7s code to support the iPhone 5 and thus can
not use any third party frameworks (as Admob etc.) until they are
updated.
That was all but just remember to test the autorotation in iOS 5 and iOS 6 because of the changes in rotation.
No.
if ([[UIScreen mainScreen] bounds].size.height > 960)
on iPhone 5 is wrong
if ([[UIScreen mainScreen] bounds].size.height == 568)
#interface UIDevice (Screen)
typedef enum
{
iPhone = 1 << 1,
iPhoneRetina = 1 << 2,
iPhone5 = 1 << 3,
iPad = 1 << 4,
iPadRetina = 1 << 5
} DeviceType;
+ (DeviceType)deviceType;
#end
.m
#import "UIDevice+Screen.h"
#implementation UIDevice (Screen)
+ (DeviceType)deviceType
{
DeviceType thisDevice = 0;
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone)
{
thisDevice |= iPhone;
if ([[UIScreen mainScreen] respondsToSelector: #selector(scale)])
{
thisDevice |= iPhoneRetina;
if ([[UIScreen mainScreen] bounds].size.height == 568)
thisDevice |= iPhone5;
}
}
else
{
thisDevice |= iPad;
if ([[UIScreen mainScreen] respondsToSelector: #selector(scale)])
thisDevice |= iPadRetina;
}
return thisDevice;
}
#end
This way, if you want to detect whether it is just an iPhone or iPad (regardless of screen-size), you just use:
if ([UIDevice deviceType] & iPhone)
or
if ([UIDevice deviceType] & iPad)
If you want to detect just the iPhone 5, you can use
if ([UIDevice deviceType] & iPhone5)
As opposed to Malcoms answer where you would need to check just to figure out if it's an iPhone,
if ([UIDevice currentResolution] == UIDevice_iPhoneHiRes ||
[UIDevice currentResolution] == UIDevice_iPhoneStandardRes ||
[UIDevice currentResolution] == UIDevice_iPhoneTallerHiRes)`
Neither way has a major advantage over one another, it is just a personal preference.
#Pascal's comment on the OP's question is right. By simply adding the image, it removes the black borders and the app will use the full height.
You will need to make adjustments to any CGRects by determining that the device is using the bigger display. I.e. If you need something aligned to the bottom of the screen.
I am sure there is a built in method, but I haven't seen anything and a lot is still under NDA so the method we use in our apps is quite simply a global function. Add the following to your .pch file and then its a simple if( is4InchRetina() ) { ... } call to make adjustments to your CGRects etc.
static BOOL is4InchRetina()
{
if (![UIApplication sharedApplication].statusBarHidden && (int)[[UIScreen mainScreen] applicationFrame].size.height == 548 || [UIApplication sharedApplication].statusBarHidden && (int)[[UIScreen mainScreen] applicationFrame].size.height == 568)
return YES;
return NO;
}
I think you can use [UIScreen mainScreen].bounds.size.height and calculate step for your objects. when you calculate step you can set coordinates for two resolutions.
Or you can get height like above and if(iphone5) then... else if(iphone4) then... else if(ipad). Something like this.
If you use storyboards then you have to create new for new iPhone i think.
As it has more pixels in height, things like GCRectMake that use coordinates won't work seamlessly between versions, as it happened when we got the Retina.
Well, they do work the same with Retina displays - it's just that 1 unit in the CoreGraphics coordinate system will correspond to 2 physical pixels, but you don't/didn't have to do anything, the logic stayed the same. (Have you actually tried to run one of your non-retina apps on a retina iPhone, ever?)
For the actual question: that's why you shouldn't use explicit CGRectMakes and co... That's why you have stuff like [[UIScreen mainScreen] applicationFrame].