Complex Multi-Device Orientation Handling - objective-c

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.

Related

My iOS app Screen is not auto sizing in iOS 5 dimension

The Screen UI is developed for 3:2, but when I run the app in iOS Simulator for iphone 5 and above, I see there is a white patch below in the bottom of the screen. The rest of the screens are appearing correctly.
- (void)applicationDidFinishLaunching:(UIApplication *)application {
[self startup];
[window addSubview:loginViewController.view];
//For Eula
NSDictionary *eulaDict = [EULAController getEULADictionary];
if (eulaDict == nil || [ self checkForVersionChange] == TRUE) {
eulaController = [[EULAController alloc] initWithNibName:#"eula"bundle:[NSBundle mainBundle]];
//temp fix as for some reason this is showing up 20 pixels shifted upwards. if this works in your scenario then remove this adjustment
CGRect frame1 = CGRectMake(0,20,320,460);
[eulaController.view setFrame:frame1];
[window addSubview:eulaController.view];
}
[self updateVersionAndBuild];
[window makeKeyAndVisible];
}
Not sure, why its not fitting the entire screen. I didn enable athe AutoLayout option but it didnt help, as during the launch of the app, it stops for iphone 5/6.
Thanks in advance !
I guess you are missing the Default-568h#2x.png default launch image...
If so, just add one to your project and it will work.
You've hardcoded the frame to iPhone 4 size:
CGRect frame1 = CGRectMake(0,20,320,460);
You've said your using autoLayout. I'd remove this and use autoLayout constraints instead and it will scale.

Camera Rotation and OverlayView in iOS8

I am experiencing an issue when using the iPad Camera in iOS 8. I've seen some older questions and a thread on the Apple Developer Forums from during the beta but still haven't find a solution.
There seems to be two parts to this issue.
1) The camera itself rotates when the device orientation rotates, eg the world is on its side
2) When opening the camera in Landscape, the overlay does not appear. When opened in Portrait it is fine.
It is an app using iOS7 as the Base SDK, problem only occurs when running the app on a device that has been upgraded to iOS8. The app is not using storyboards, it is using nibs.
I'm hoping to push out a fix for this with Xcode 5.1.1 before moving onto the iOS8 specific fixes and using it as a Base SDK in the next version.
Here is my code to bring up the camera:
if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera] == YES) {
// Create Camera
imagePicker = [[UIImagePickerController alloc] init];
imagePicker.sourceType = UIImagePickerControllerSourceTypeCamera;
imagePicker.delegate = self;
imagePicker.showsCameraControls = NO;
// Set up custom controls view
[[NSBundle mainBundle] loadNibNamed:#"OverlayView" owner:self options:nil];
self.overlayView.frame = imagePicker.cameraOverlayView.frame;
imagePicker.cameraOverlayView = self.overlayView;
self.overlayView = nil;
// Show Camera
[self presentViewController:imagePicker animated:NO completion:nil];
[imagePicker release];
}
I have also tried
And the Layout of the Toolbar (sitting at the bottom) of the OverlayView:
If I change that to sit "at the top" it appears in both portrait and landscape! So it must have to do with the view/window/something size, though it's strange how its behaviour would change when the layout has stayed the same.
I have tried it with both showsCameraControls = YES and hashing out the OverlayView block, and problem #1 persists so it's not to do with the overlay at app.
I'm hoping someone has found an answer to this, it seems like quite a common problem.
Please let me know if you need any further details.
Edit 1: Fixed the Overlay (Issue #2)
It wasn't applying the orientation to the OverlayView, fixed it like this:
// Grab the window frame and adjust it for orientation - from http://stackoverflow.com/a/15707997/520902
UIView *rootView = [[[UIApplication sharedApplication] keyWindow]
rootViewController].view;
CGRect originalFrame = [[UIScreen mainScreen] bounds];
CGRect screenFrame = [rootView convertRect:originalFrame fromView:nil];
...
self.overlayView.frame = imagePicker.cameraOverlayView.frame;
I suspect that it's related to the camera not realising it's orientated too, will keep searching for a fix for Problem #1.
Edit 2: Update on Issue #1
Looks like the camera rotating might be an Apple issue. On iOS8 if you open up the Contacts app, edit a contact and choose 'Take Photo', the exact same issue occurs - in a default Apple app!
I still can't find a fix so I am just destroying and recreating the imagePicker on each orientation change for now, it's ugly but will suffice until Apple release a fix or a better solution pops up.
Apple fixed this problem in iOS 8.1.

How can I get my app ready for the iPhone 5 [duplicate]

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].

if (device == iPad), if (device == iPhone)

So I have a universal app and I'm setting up the content size of a UIScrollView. Obviously the content size is going to be different on iPhones and iPads. How can I set a certain size for iPads and another size for iPhones and iPod touches?
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)
{
// The device is an iPad running iOS 3.2 or later.
}
else
{
// The device is an iPhone or iPod touch.
}
if(UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone)
and
if(UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)
The macros UI_USER_INTERFACE_IDIOM() also works on older iOS versions like iOS 3.0 without crashing.
UI_USER_INTERFACE_IDIOM() is the best solution in your case since your app is universal. But if you are running an iPhone app on iPad than UI_USER_INTERFACE_IDIOM() will return UIUserInterfaceIdiomPhone, regardless of the device. For such purposes as that, you can use the UIDevice.model property:
if ([[UIDevice currentDevice].model rangeOfString:#"iPad"].location != NSNotFound) {
//Device is iPad
}
In Swift 2.x you can use the following equalities to determine the kind of device:
UIDevice.currentDevice().userInterfaceIdiom == .Phone
or
UIDevice.currentDevice().userInterfaceIdiom == .Pad
In Swift 3 for new people coming here.
if UIDevice.current.userInterfaceIdiom == .pad {
\\ Available Idioms - .pad, .phone, .tv, .carPlay, .unspecified
\\ Implement your awesome logic here
}
The UIDevice class will tell you everything you need to know about the device. The model property, for instance, will tell you the model of the device. You can use this to determine which view to use for the current device.
Use the UIScreen class to determine the application's frame.
CGRect usableSpace = [[UIScreen mainScreen] applicationFrame];
The returned rectangle is the screen's size minus the status bar. Don't use the UI idioms for determining available space as they're not future-proof.
By the way, UIViewController can resize content views (including scroll views) automatically, as well as provide you with other goodies, such as auto-rotation. Consider it if it's appropriate in your situation.
if you are using swift,
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiom.Pad)
{
// device is ipad
}
else
{
//device is iPhone
}
You can also check the UIUSerInterfaceIdiom and choose the device you want to UIUserInterfaceIdiom.Pad or UIUserInterfaceIdiom.Phone or UIUserInterfaceIdiom.TV or UIUserInterfaceIdiom.CarPlay

iPad - xib ignoring orientation on view loads

iPad question:
We have views that we're initializing from .xibs, with each .xib containing both landscape and portrait layouts.
The first view we load respects the orientation of the device, but any subsequent views seem to have confusion over which orientation they should be using. Or, rather, they seem to ignore orientation altogether and go with whichever orientation the .xib file was saved with. (i.e. We saved a .xib file in landscape, and even though the device was held in portrait position, it loaded the view from the .xib with a landscape layout.)
This is how we're calling in these views:
Settings *settingsTEMP = [[Settings alloc] init];
self.settings = settingsTEMP;
[self.view insertSubview:settings.view atIndex:0];
[settingsTEMP release];
The first of the calls (the one that respects the device's orientation) is in the viewDidLoad. The second, which uses a different view (the one that doesn't respect the device's orientation) is in a method that's called after a button is pressed, but uses the same syntax.
If we put both into viewDidLoad, they both respect the orientation.
We've tried searching for anyone having similar issues but have been thus far unsuccessful. Thanks in advance for any help you can provide.
I use the following
UIInterfaceOrientation toInterfaceOrientation = self.interfaceOrientation;
if ((toInterfaceOrientation == UIDeviceOrientationLandscapeLeft) ||
(toInterfaceOrientation == UIDeviceOrientationLandscapeRight))
{
NSLog(#"vSettings UIDeviceOrientationLandscape");
CGRect contentRect = CGRectMake(0,0,1024,768);
vSettings.bounds = contentRect;
CGRect myFrame = vSettings.frame;
myFrame.origin.x = 0.0;
myFrame.origin.y = 0.0;
vSettings.frame = myFrame;
UIImage *image;
image = [UIImage imageNamed: #"Default-Landscape.png"];
[backGroundSettings setImage:image];
}
I hate having to do this as it seems a right fudge but it works for me. Called just after I create the view.