Window has weird frame and offset after rotation on iOS 8 - ios

On iOS 8, when launching the app in landscape mode, I get weird values for the key window's frame after a rotation:
// Before rotation (landscape)
{{0, 0}, {768, 1024}}
// After rotation (portrait)
{{-256, 0}, {1024, 768}}
Code:
NSLog(#"%#", NSStringFromCGRect(UIApplication.sharedApplication.keyWindow.frame));
Where does the X offset come from, and why are the frame values inverted (should be width=768 in landscape mode)?

I was having this problem and fixed it by using the fixedCoordinateSpace.bounds. Heres a quick example.
Observe status bar changes in the window.
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(statusBarFrameOrOrientationDidChanged) name:UIApplicationDidChangeStatusBarOrientationNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(statusBarFrameOrOrientationDidChanged) name:UIApplicationDidChangeStatusBarFrameNotification object:nil];
}
return self;
}
Than rotate the window and resize the windows frame.
- (void)statusBarFrameOrOrientationDidChanged {
CGRect rect = ({
CGRect rect;
if ([[[UIDevice currentDevice] systemVersion] intValue] <= 7) {
rect = [UIScreen mainScreen].bounds;
} else {
id<UICoordinateSpace> fixedCoordinateSpace = [UIScreen mainScreen].fixedCoordinateSpace;
rect = fixedCoordinateSpace.bounds;
}
rect;
});
CGAffineTransform transform = CGAffineTransformMakeRotation(({
CGFloat angle;
switch ([UIApplication sharedApplication].statusBarOrientation) {
case UIInterfaceOrientationPortraitUpsideDown:
angle = M_PI;
break;
case UIInterfaceOrientationLandscapeLeft:
angle = -M_PI_2;
break;
case UIInterfaceOrientationLandscapeRight:
angle = M_PI_2;
break;
default:
angle = 0.0f;
break;
}
angle;
}));
if (!CGAffineTransformEqualToTransform(self.transform, transform)) {
self.transform = transform;
}
if (!CGRectEqualToRect(self.frame, rect)) {
self.frame = rect;
}
}

Related

How to stick UIView to top of UIWindow on iOS 7

I have a custom UIView which I want to stick to the top of the UIWindow even when interface orientation changes. Here is the my view in portrait orientation
The problem is that prior iOS 8 UIWindow coordinate system is not being changed with the orientation changes. So I need to make all the calculations by hand.
The first thing is to change the transform of the UIView, which I do using this method
-(CGFloat)angleForOrientation:(UIInterfaceOrientation)orientation
{
CGFloat angle;
switch (orientation) {
case UIInterfaceOrientationLandscapeLeft:
angle = -M_PI /2.0;
NSLog(#"UIInterfaceOrientationLandscapeLeft");
break;
case UIInterfaceOrientationLandscapeRight:
angle = M_PI /2.0;
NSLog(#"UIInterfaceOrientationLandscapeRight");
break;
case UIInterfaceOrientationPortraitUpsideDown:
angle = M_PI;
NSLog(#"UIInterfaceOrientationPortraitUpsideDown");
break;
default:
angle = 0;
NSLog(#"UIInterfaceOrientationPortrait");
break;
}
return angle;
}
The second thing is to somehow map actual coordinate systems to UIWindow coordinate system, which stays the same.
So how should I calculate the frame of the custom UIView that even when the user rotates the view to other orientations I will have the same sized UIView sticked to the top centre of the screen?
For instance this is how should the view look like in landscape
By the way this image is generated from iOS 8 version. For which I do the following
self.frame = CGRectMake(window.bounds.size/2-50, 0, 100, 100);
CGFloat angle = [self angleForOrientation:orientation];
self.transform = CGAffineTransformMakeRotation(angle);
I need to do something similar to iOS 7. How can I achieve this?
Thanks for help!
So Finally I figured out how to implement this.
I've created the following method to calculate the rect which will stick the view to top based on given bounds.
-(CGRect)getTopRectForBounds:(CGRect)bounds orientation:(UIInterfaceOrientation)orientation window:(UIWindow *)window
{
CGRect newRect;
CGFloat statusBarHeight = [self getStatusBarHeight];
CGSize screenSize = window.screen.bounds.size;
CGFloat sW = screenSize.width;
CGFloat sH = screenSize.height;
CGFloat W = rect.size.width;
CGFloat H = rect.size.height;
switch (orientation) {
case UIInterfaceOrientationLandscapeLeft:
newRect = CGRectMake(statusBarHeight, (sH-W)/2, H,W);
break;
case UIInterfaceOrientationLandscapeRight:
newRect = CGRectMake(sW-H-statusBarHeight, (sH-W)/2, H,W);
break;
case UIInterfaceOrientationPortraitUpsideDown:
newRect = CGRectMake((sW-W)/2, sH-H-statusBarHeight, W,H);
break;
default:
newRect = CGRectMake((sW-W)/2, statusBarHeight, W,H);
break;
}
return newRect;
}
Then I just change the frame whenever orientation changes.
So first I listen to Orientation changes
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(statusBarFrameOrOrientationChanged:) name:UIApplicationDidChangeStatusBarOrientationNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(statusBarFrameOrOrientationChanged:) name:UIApplicationDidChangeStatusBarFrameNotification object:nil];
Then in the orientation change event handler and change the transform and the frame. (see my question to see the method which handles the transforms)
CGRect bounds = CGRectMake(0, 0, 100, 100);
UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation;
self.frame = [self getTopRectForBounds:bounds orientation:orientation window:self.window]
CGFloat angle = [self angleForOrientation:orientation];
self.transform = CGAffineTransformMakeRotation(angle);

iOS 8 / 7 UIWindow with UIWindowLevelStatusBar Rotation issue

I'm trying to add a UIWindow with UIWindowLevelStatusBar level. it works in portrait mode on ipad perfectly but after rotating device its messed up.
on iOS 7.x all rotations are fine except for upside-down, and on iOS 8.x only portrait mode is fine and all other orientations are messed up. any idea how to solve that?
CGRect frame = [UIApplication sharedApplication].statusBarFrame;
self.statusWindow = [[UIWindow alloc] initWithFrame:CGRectMake(frame.origin.x, frame.origin.y, frame.size.width, 20)];
UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation];
CGRect frame = [UIApplication sharedApplication].statusBarFrame;
CGAffineTransform test = [self transformForOrientation:orientation];
[self.statusWindow setWindowLevel:UIWindowLevelStatusBar];
[self.statusWindow setHidden:NO];
- (CGAffineTransform)transformForOrientation:(UIInterfaceOrientation)orientation
{
switch (orientation)
{
case UIInterfaceOrientationLandscapeLeft:
return CGAffineTransformMakeRotation(-DEGREES_TO_RADIANS(90.0f));
case UIInterfaceOrientationLandscapeRight:
return CGAffineTransformMakeRotation(DEGREES_TO_RADIANS(90.0f));
case UIInterfaceOrientationPortraitUpsideDown:
return CGAffineTransformMakeRotation(DEGREES_TO_RADIANS(180.0f));
case UIInterfaceOrientationPortrait:
default:
return CGAffineTransformMakeRotation(DEGREES_TO_RADIANS(0.0f));
}
}
In your UIWindow subclass' init method, observe this notification:
// Rotation Notifications
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(didChangeOrientation:) name:UIDeviceOrientationDidChangeNotification object:nil];
self.baseT = self.transform;
Then the method itself:
- (void)didChangeOrientation:(NSNotification *)n
{
UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation;
switch (orientation) {
case UIInterfaceOrientationPortrait:
self.transform = CGAffineTransformRotate(self.baseT, 0);
break;
case UIInterfaceOrientationPortraitUpsideDown:
self.transform = CGAffineTransformRotate(self.baseT, M_PI);
break;
// Home button on left
case UIInterfaceOrientationLandscapeLeft:
self.transform = CGAffineTransformRotate(self.baseT, -M_PI_2);
break;
case UIInterfaceOrientationLandscapeRight:
self.transform = CGAffineTransformRotate(self.baseT, M_PI_2);
break;
default:
self.transform = CGAffineTransformMakeRotation(0);
break;
}
}
Depending on your implementation, you may have to also ensure the frame doesn't change. Also, in the Class' dealloc method, stop listening to the notifications.
As a modification to #dezinezync's answer: performing the rotation would work better if you put it in the willRotateToInterfaceOrientation method of the view controller. This way, the rotation will only be applied if the device is rotated to one of the supported orientations specified in the General -> Deployment Info section of the app's build settings. As an added bonus, you get access to the duration of the rotation animation. Just make sure you have a reference to your modal window in the view controller.
The UIDeviceOrientationDidChangeNotification notification is fired whenever the screen orientation changes, regardless of what orientations are supported. This can lead to undesireable behavior.
Here's an example:
UIWindow *modalWindow;
// Initialize your modal window somewhere in your code
// ...
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
[super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration];
// Make sure the modalWindow exists
if (modalWindow != nil)
{
// Animate the modal window's rotation
[UIView animateWithDuration:duration animations:^{
[self rotateModal:modalWindow];
}];
}
}
- (void)rotateModal:(UIWindow*)window
{
UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation;
switch (orientation) {
case UIInterfaceOrientationPortrait:
window.transform = CGAffineTransformRotate(self.baseT, 0);
break;
case UIInterfaceOrientationPortraitUpsideDown:
window.transform = CGAffineTransformRotate(self.baseT, M_PI);
break;
// Home button on left
case UIInterfaceOrientationLandscapeLeft:
window.transform = CGAffineTransformRotate(self.baseT, -M_PI_2);
break;
case UIInterfaceOrientationLandscapeRight:
window.transform = CGAffineTransformRotate(self.baseT, M_PI_2);
break;
default:
window.transform = CGAffineTransformMakeRotation(0);
break;
}
}
I just did something like this on a landscape-only app, and I finally got it working properly by moving everything to the willRotateToInterfaceOrientation method.
Solution for iOS8 (need a transform as showed above as well):
UIScreen *screen = [UIScreen mainScreen];
CGRect statusBarFrame = [UIApplication sharedApplication].statusBarFrame;
if ([screen respondsToSelector:#selector(fixedCoordinateSpace)])
{
[self setFrame:[screen.coordinateSpace convertRect:statusBarFrame toCoordinateSpace:screen.fixedCoordinateSpace]];
}
However it does not work for iOS9 beta.

scale image and button orientation correctly for landscape and portrait mode ipad

Developed a ipad application which looks good in portrait mode.Like following:
But, not looking good in landscape mode.
How to make this correct? I have autolayout option enabled...
Note: I'm adding back ground iamge as
self.view.backgrounfcolor = [UIColor colorwithparttenimage:[UIImage imageNamed:#"ems.png"]];
Here is just for reference . just copied and pasting from my repository . Change accordingly .
-(void)viewWillAppear:(BOOL)animated{
[[self navigationController] setNavigationBarHidden:YES animated:NO];
[[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
[self orientationChanged:(UIInterfaceOrientation)[[UIDevice currentDevice]orientation ]];
}
-(BOOL) supportedInterfaceOrientations{
return UIInterfaceOrientationMaskAllButUpsideDown;
}
-(BOOL) shouldAutorotate{
return YES;
}
-(void) willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration{
[self orientationChanged:toInterfaceOrientation];
}
-(void) orientationChanged: (UIInterfaceOrientation)orientation{
if(orientation == UIInterfaceOrientationPortrait || orientation == UIInterfaceOrientationPortraitUpsideDown){
NSLog(#"Changed Orientation To Portrait");
// self.viewPortrait.hidden = NO;
// self.viewLandscape.hidden = YES;
[self portraitOrientation];
}
else if(orientation == UIInterfaceOrientationLandscapeLeft || orientation == UIInterfaceOrientationLandscapeRight){
NSLog(#"Changed Orientation To Landscape");
//self.viewPortrait.hidden = YES;
//self.viewLandscape.hidden = NO;
[self landscapeLeftOrientation];
/*
if(deviceOrientation ==UIInterfaceOrientationLandscapeLeft){
NSLog(#"Changed Orientation To Landscape left");
[self landscapeLeftOrientation];
}else{
NSLog(#"Changed Orientation To Landscape right");
[self landscapeRightOrientation];
}
*/
}
}
-(void)landscapeLeftOrientation{
// Rotates the view.
NSLog(#"LandscapeLeft");
CGAffineTransform transform = CGAffineTransformMakeRotation(0);
self.view.transform = transform;
// Repositions and resizes the view.
CGRect contentRect = CGRectMake(0, 0, 480, 320);
self.view.bounds = contentRect;
self.view.bounds = contentRect;
self.img.center = CGPointMake(256, 23);
self.btnHome.center = CGPointMake(437.0f, 23.0f);
self.messageList.frame = CGRectMake(54, 54, 405,200);
self.txtMessage.frame = CGRectMake(26, 262, 355, 30);
self.btnSendMsg.frame = CGRectMake(399, 262,61,32);
}
-(void)portraitOrientation{
// Rotates the view.
CGAffineTransform transform = CGAffineTransformMakeRotation(0);
self.view.transform = transform;
// Repositions and resizes the view.
CGRect contentRect = CGRectMake(0, 0, 320, 480);
self.view.bounds = contentRect;
self.img.center = CGPointMake(160.0f, 24.0f);
self.btnHome.center = CGPointMake(277.0f, 24.0f);
self.messageList.frame = CGRectMake(3,49, 341,321);
self.txtMessage.frame = CGRectMake(3, 378, 235, 30);
self.btnSendMsg.frame = CGRectMake(256, 378,61,32);
}

iPad iOS7 - UIImagePickerController in UIPopoverController has wrong preview image

I am using an UIImagePickerController within an UIPopoverController which is working perfectly with iOS6. With iOS 7 the "preview" image which is shown to capture the image is rotated, but if I take a picture it is saved correctly.
This is how I get my picker:
UIImagePickerController *imagePicker = [[UIImagePickerController alloc] init];
imagePicker.delegate = self;
imagePicker.sourceType = UIImagePickerControllerSourceTypeCamera;
imagePicker.mediaTypes = [NSArray arrayWithObjects:
(NSString *) kUTTypeImage,
nil];
imagePicker.allowsEditing = NO;
And add it to a popover controller:
self.imagePickerPopOver = [[UIPopoverController alloc] initWithContentViewController:imagePicker];
[self.imagePickerPopOver presentPopoverFromRect:CGRectMake(aPosViewA.x, cameraButton_y, 100.0, 30.0) inView:self.detailViewController.view permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
Those are calculations for the button position at a UIScrollView to show the popover at the correct position:
presentPopoverFromRect:CGRectMake(aPosViewA.x, cameraButton_y, 100.0, 30.0)
I don't think that the problem lies there as I have tried out several combinations.
I have also tried to capture the image in fullscreen-mode, but the app is only allowed to use landscape mode. If the image is taken in portrait-mode and the modal view is dismissed, the app stays in portrait mode as well. I couldn't find a way to prevent the UIImagePickerController to switch to portrait mode or to force the app back to landscape mode if the modal view was dismissed.
UPDATE
I have taken the answer from here and came a step further.
I transform the view after creating the picker and before showing the popover :
switch ([UIApplication sharedApplication].statusBarOrientation) {
case UIInterfaceOrientationLandscapeLeft:
self.imagePicker.view.transform = CGAffineTransformMakeRotation(M_PI/2);
break;
case UIInterfaceOrientationLandscapeRight:
self.imagePicker.view.transform = CGAffineTransformMakeRotation(-M_PI/2);
break;
default:
break;
}
which works as long as i don't turn around the iPad. For that I am registering for the orientation changed event:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(orientationChanged:) name:UIDeviceOrientationDidChangeNotification object:nil];
and change the picker view:
- (void)orientationChanged:(NSNotification *)notification{
if (self.imagePicker) {
switch ([UIApplication sharedApplication].statusBarOrientation) {
case UIInterfaceOrientationLandscapeLeft:
self.imagePicker.view.transform = CGAffineTransformMakeRotation(M_PI/2);
break;
case UIInterfaceOrientationLandscapeRight:
self.imagePicker.view.transform = CGAffineTransformMakeRotation(-M_PI/2);
break;
default:
break;
}
}
}
REMAINING PROBLEM:
As I wrote in the beginning, when the picture was taken, it was shown correctly to accept or dismiss it. This is now transformed as well. Somehow I need to know when the image is taken and transform it back.
AND, this is really a nasty hack and probably won't work with the next iOS Update. Has anybody an idea how to implement that in a cleaner way?
UPDATE 2
This was too nasty, I have found a cleaner solution which solves my problem but is not the answer to the initial question regarding an imagepicker in a popover controller, which is not recommended by Apple, but allowed.
I have subclassed now the UIImagePickerController like this:
#implementation QPImagePickerController
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation {
return UIInterfaceOrientationIsLandscape(toInterfaceOrientation);
}
- (BOOL)shouldAutorotate {
return YES;
}
- (NSUInteger)supportedInterfaceOrientations{
return UIInterfaceOrientationMaskLandscape;
}
#end
and I am using the imagepicker in fullscreen instead in a popover. Tested so far in iOS7.
The UIImagePickerController has a property called cameraViewTransform. Applying a CGAffineTransform to this will transform the preview image but will not transform the captured image which will therefore be correctly captured. I have the same problem that you describe and I solved it (for iOS7) by creating my camera controller and placing it in a popover as follows:
UIImagePickerController *imagePickerController = [[UIImagePickerController alloc] init];
[imagePickerController setDelegate:self];
imagePickerController.sourceType = UIImagePickerControllerSourceTypeCamera;
CGFloat scaleFactor=1.3f;
switch ([UIApplication sharedApplication].statusBarOrientation) {
case UIInterfaceOrientationLandscapeLeft:
imagePickerController.cameraViewTransform = CGAffineTransformScale(CGAffineTransformMakeRotation(M_PI * 90 / 180.0), scaleFactor, scaleFactor);
break;
case UIInterfaceOrientationLandscapeRight:
imagePickerController.cameraViewTransform = CGAffineTransformScale(CGAffineTransformMakeRotation(M_PI * -90 / 180.0), scaleFactor, scaleFactor);
break;
case UIInterfaceOrientationPortraitUpsideDown:
imagePickerController.cameraViewTransform = CGAffineTransformMakeRotation(M_PI * 180 / 180.0);
break;
default:
break;
}
__popoverController = [[UIPopoverController alloc] initWithContentViewController:imagePickerController];
[__popoverController presentPopoverFromRect:presentationRect inView:self.view permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
I also scale the image when in Landscape so that it fills the viewfinder more than it otherwise would. To my mind this is all rather nasty, but I will hopefully be able to remove it once iOS7.1 arrives.
I've found another solution which handles the case where a device is rotated while the UIIMagePickerView is presenting based on the answer Journeyman provided. It also handles the case where the view is rotated from UIOrientationLandscapeRight/UIOrientationLandscapeLeft back to UIOrientationPortrait.
I created a subclass of UIImagePickerController:
#import <UIKit/UIKit.h>
#interface PMImagePickerController : UIImagePickerController
#end
Then registered it to receive notifications if the device orientation changes:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(fixCameraOrientation:) name:UIDeviceOrientationDidChangeNotification object:nil];
The selector fixCameraOrientation contains Journeyman's code with one extra case, wrapped in a check to make sure the sourceType is the camera:
- (void)fixCameraOrientation:(NSNotification*)notification
{
if (self.sourceType == UIImagePickerControllerSourceTypeCamera) {
CGFloat scaleFactor=1.3f;
switch ([UIApplication sharedApplication].statusBarOrientation) {
case UIInterfaceOrientationLandscapeLeft:
self.cameraViewTransform = CGAffineTransformScale(CGAffineTransformMakeRotation(M_PI * 90 / 180.0), scaleFactor, scaleFactor);
break;
case UIInterfaceOrientationLandscapeRight:
self.cameraViewTransform = CGAffineTransformScale(CGAffineTransformMakeRotation(M_PI * -90 / 180.0), scaleFactor, scaleFactor);
break;
case UIInterfaceOrientationPortraitUpsideDown:
self.cameraViewTransform = CGAffineTransformMakeRotation(M_PI * 180 / 180.0);
break;
case UIInterfaceOrientationPortrait:
self.cameraViewTransform = CGAffineTransformIdentity;
break;
default:
break;
}
}
}
The important case here is the case where the device orientation goes to portrait. The overlay's view needs to be reset in this case. It's also important for the fixCameraOrientation selector to be called after the image picker view loads, in case the device is rotated:
- (void)viewDidLoad
{
[super viewDidLoad];
[self fixCameraOrientation:nil];
}
I had a similar situation in my app. However the preview was rotating correctly in iOS7 and not in iOS8. This code assumes you have more than one orientation.
The first thing is to subclass UIImagePickerController.
Starting from the top, add #import <AVFoundation/AVFoundation.h> to your .m file.
Also add a property to save the initial orientation #property (nonatomic) UIInterfaceOrientation startingOrientation; and another for a condition to remove clipping #property (nonatomic) BOOL didAttemptToRemoveCropping;.
Were going to listen to a couple of notifications.
UIApplicationDidChangeStatusBarOrientationNotification is obviously to listen for the device rotation. AVCaptureSessionDidStartRunningNotification is called right when the camera starts capturing.
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(statusBarOrientationDidChange:) name:UIApplicationDidChangeStatusBarOrientationNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(captureSessionDidStart:) name:AVCaptureSessionDidStartRunningNotification object:nil];
In the -captureSessionDidStart: add a condition to verify the view is actually on screen and to make sure the camera is supposed to be displayed if (self.view.window && self.sourceType == UIImagePickerControllerSourceTypeCamera). If so, set the starting orientation self.startingOrientation = [UIApplication sharedApplication].statusBarOrientation;.
In the -statusBarOrientationDidChange: add the same condition as above, but this time if true, we'll update the camera transform. First we get the offset rotation based on the initial rotation. This is needed when you enter the UIImagePickerController in orientations other then portrait.
CGFloat startingRotation = ({
CGFloat rotation;
switch (self.startingOrientation) {
case UIInterfaceOrientationPortraitUpsideDown:
rotation = M_PI;
break;
case UIInterfaceOrientationLandscapeLeft:
rotation = -M_PI_2;
break;
case UIInterfaceOrientationLandscapeRight:
rotation = M_PI_2;
break;
default:
rotation = 0.0f;
break;
}
rotation;
});
Next we'll update the camera transform with the current rotation.
self.cameraViewTransform = CGAffineTransformMakeRotation(({
CGFloat angle;
switch ([UIApplication sharedApplication].statusBarOrientation) {
case UIInterfaceOrientationPortraitUpsideDown:
angle = startingRotation + M_PI;
break;
case UIInterfaceOrientationLandscapeLeft:
angle = startingRotation + M_PI_2;
break;
case UIInterfaceOrientationLandscapeRight:
angle = startingRotation + -M_PI_2;
break;
default:
angle = startingRotation;
break;
}
angle;
}));
And finally we'll attempt to remove the black bars presented in either 90 degree orientation from the starting orientation. (This might only be an iOS8 issue.) In slightly more detail, if I enter the UIImagePickerController in portrait mode and then rotate to landscape, there will be black bars on the top and bottom of the preview. The solution for this is not to scale but rather to remove the clipping of a superview. We only need to make this attempt once, so first check if we have called this code. Also make sure that we only call this code if we have rotated. If its called in the initial orientation it wont work right away.
if (!self.didAttemptToRemoveCropping && self.startingOrientation != [UIApplication sharedApplication].statusBarOrientation) {
self.didAttemptToRemoveCropping = YES;
[self findClippedSubviewInView:self.view];
}
Finally in the code for -findClippedSubviewInView: we loop through all the subviews to search for a view with .clipsToBounds = YES. If thats true we make one more condition to verify one of its ancestral superviews is correct.
for (UIView* subview in view.subviews) {
if (subview.clipsToBounds) {
if ([self hasAncestorCameraView:subview]) {
subview.clipsToBounds = NO;
break;
}
}
[self findClippedSubviewInView:subview];
}
In the -hasAncestorCameraView: we simply loop up the superview chain and return true if one of the classes has CameraView in the name.
if (view == self.view) {
return NO;
}
NSString* className = NSStringFromClass([view class]);
if ([className rangeOfString:#"CameraView"].location != NSNotFound) {
return YES;
} else {
return [self hasAncestorCameraView:view.superview];
}
Thats the breakdown of code, here's it all together.
#import <AVFoundation/AVFoundation.h>
#import "GImagePickerController.h"
#interface GImagePickerController ()
#property (nonatomic) UIInterfaceOrientation startingOrientation;
#property (nonatomic) BOOL didAttemptToRemoveCropping;
#end
#implementation GImagePickerController
- (instancetype)init {
self = [super init];
if (self) {
if ([[[UIDevice currentDevice] systemVersion] intValue] >= 8) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(statusBarOrientationDidChange:) name:UIApplicationDidChangeStatusBarOrientationNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(captureSessionDidStart:) name:AVCaptureSessionDidStartRunningNotification object:nil];
}
}
return self;
}
- (void)dealloc {
if ([[[UIDevice currentDevice] systemVersion] intValue] >= 8) {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
}
#pragma mark - Capture Session
- (void)captureSessionDidStart:(NSNotification *)notification {
if (self.view.window && self.sourceType == UIImagePickerControllerSourceTypeCamera) {
[self updateStartingOrientation];
}
}
#pragma mark - Orientation
- (void)updateStartingOrientation {
self.startingOrientation = [UIApplication sharedApplication].statusBarOrientation;
[self updateCameraTransform];
}
- (void)updateCameraTransform {
CGFloat startingRotation = ({
CGFloat rotation;
switch (self.startingOrientation) {
case UIInterfaceOrientationPortraitUpsideDown:
rotation = M_PI;
break;
case UIInterfaceOrientationLandscapeLeft:
rotation = -M_PI_2;
break;
case UIInterfaceOrientationLandscapeRight:
rotation = M_PI_2;
break;
default:
rotation = 0.0f;
break;
}
rotation;
});
self.cameraViewTransform = CGAffineTransformMakeRotation(({
CGFloat angle;
switch ([UIApplication sharedApplication].statusBarOrientation) {
case UIInterfaceOrientationPortraitUpsideDown:
angle = startingRotation + M_PI;
break;
case UIInterfaceOrientationLandscapeLeft:
angle = startingRotation + M_PI_2;
break;
case UIInterfaceOrientationLandscapeRight:
angle = startingRotation + -M_PI_2;
break;
default:
angle = startingRotation;
break;
}
angle;
}));
if (!self.didAttemptToRemoveCropping && self.startingOrientation != [UIApplication sharedApplication].statusBarOrientation) {
self.didAttemptToRemoveCropping = YES;
[self findClippedSubviewInView:self.view];
}
}
- (void)statusBarOrientationDidChange:(NSNotification *)notification {
if (self.view.window && self.sourceType == UIImagePickerControllerSourceTypeCamera) {
[self updateCameraTransform];
}
}
#pragma mark - Remove Clip To Bounds
- (BOOL)hasAncestorCameraView:(UIView *)view {
if (view == self.view) {
return NO;
}
NSString* className = NSStringFromClass([view class]);
if ([className rangeOfString:#"CameraView"].location != NSNotFound) {
return YES;
} else {
return [self hasAncestorCameraView:view.superview];
}
}
- (void)findClippedSubviewInView:(UIView *)view {
for (UIView* subview in view.subviews) {
if (subview.clipsToBounds) {
if ([self hasAncestorCameraView:subview]) {
subview.clipsToBounds = NO;
break;
}
}
[self findClippedSubviewInView:subview];
}
}
#end
iPad with Camera - don't display in a popover. Instead, present in a modal view controller, like you would on the iPhone. (at least starting with iOS 7)

iOS CAlayer Orientation AVCaptureVideoPreviewLayer doesn't rotate

Summary:
I can't force the CALayer to respond correctly to orientation changes.
Whenever I try to use cgaffinetransform I am getting weird results (layer is not centered).
Any help will be appreciated!
thanks,
Process
I am adding a preview of video using AVCaptureVideoPreviewLayer subclass. When device is in a portrait orientation everything looks fine. The problem appears when device is rotated to landscape orientation (left or right) or portrait upside down.
I am adding a preview of video using AVCaptureVideoPreviewLayer subclass. When device is in a portrait orientation everything looks fine. The problem appears when device is rotated to landscape orientation (left or right) or portrait upside down.
I am adding a preview layer using the following code:
CGRect layerRect = [[[self view] layer] bounds];
[[[self captureManager] previewLayer] setBounds:layerRect];
[[[self captureManager] previewLayer]setFrame:CGRectMake(0, height, width, height)];
[[[self captureManager] previewLayer] setPosition:CGPointMake(CGRectGetMidX(layerRect),CGRectGetMidY(layerRect))];
And it is displayed properly in portrait mode.
When I try to rotate the device, preview layer behaves weird. It seems like it doesn't resize itself, and it doesn't rotate correctly.
I tried to fix it by adding the following method
-(void)rotateLayer{
CALayer * stuckview = [[self captureManager] previewLayer];
CGRect layerRect = [[[self view] layer] bounds];
UIDeviceOrientation orientation =[[UIDevice currentDevice]orientation];
switch (orientation) {
case UIDeviceOrientationLandscapeLeft:
stuckview.affineTransform = CGAffineTransformMakeRotation(M_PI+ M_PI_2); // 270 degress
NSLog(#"Landscape Left");
[stuckview setPosition: CGPointMake(self.view.bounds.size.width /2.0, self.view.bounds.size.height /2.0)];
break;
case UIDeviceOrientationLandscapeRight:
stuckview.affineTransform = CGAffineTransformMakeRotation(M_PI_2); // 90 degrees
NSLog(#"Landscape Right");
[stuckview setPosition: CGPointMake(self.view.bounds.size.width /2.0, self.view.bounds.size.height /2.0)];
break;
case UIDeviceOrientationPortraitUpsideDown:
stuckview.affineTransform = CGAffineTransformMakeRotation(M_PI); // 180 degrees
NSLog(#"Landscape Upside down");
break;
default:
stuckview.affineTransform = CGAffineTransformMakeRotation(0.0);
break;
}
float h1 = stuckview.frame.size.height;
float w1 = stuckview.frame.size.width;
if(UIDeviceOrientationIsPortrait(orientation))
{
stuckview.position =CGPointMake(h1/2.0, w1/2.0);
NSLog(#"Portrait");
}
else{
stuckview.position =CGPointMake(w1/2.0, h1/2.0);
NSLog(#"Portrait");
}
}
After adding the method above I can see a progress. Now layer rotates correctly reflecting current device orientation and it's displayed correctly in landscape mode, but NOT in portrait mode.
The layer is not positioned correctly, it isn't centered on the screen (look at the screenshot). To see what's happening I added following debug statements:
CALayer * stuckview = [[self captureManager] previewLayer];
CGRect layerRect = [[[self view] layer] bounds];
float h = stuckview.bounds.size.height;
float w = stuckview.bounds.size.width;
float x = stuckview.bounds.origin.x;
float y = stuckview.bounds.origin.y;
float h1 = stuckview.frame.size.height;
float w1 = stuckview.frame.size.width;
float x1 = stuckview.frame.origin.x;
float y1 = stuckview.frame.origin.y;
NSLog(#"%f %f %f %f ", h,w,x,y );
NSLog(#"%f %f %f %f ", h1,w1,x1,y1 );
NSLog(#"Anchor Point: %f %f",stuckview.anchorPoint.x, stuckview.anchorPoint.y);
NSLog(#"Position: %f %f",stuckview.position.x, stuckview.position.y);
CGAffineTransform at = stuckview.affineTransform;
NSLog(#"Affine Transform After : %f %f %f %f %f %f %f", at.a,at.b, at.c, at.d, at.tx,at.tx, at.ty);
And get the following output:
2012-09-30 13:25:12.067 RotatePreviewLayer[2776:907] 1024.000000 768.000000 0.000000 0.000000
2012-09-30 RotatePreviewLayer[2776:907] 1024.000000 768.000000 128.000000 -128.000000
2012-09-30 13:25:12.070 RotatePreviewLayer[2776:907] Portrait
2012-09-30 13:25:12.072 RotatePreviewLayer[2776:907] Anchor Point: 0.500000 0.500000
2012-09-30 13:25:12.074 RotatePreviewLayer[2776:907] Position: 512.000000 384.000000
2012-09-30 13:25:12.076 RotatePreviewLayer[2776:907] Affine Transform after: -1.000000 0.000000 -0.000000 -1.000000 0.000000 0.000000 0.000000
Notice the second line of the debug output. The frame of the preview layer is moved by 128,-128.
Can anyone explain me why is this happening and how to fix the orientation issues with the preview layer?
thank you,
Janusz
What about this?
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
_videoPreviewLayer.connection.videoOrientation = toInterfaceOrientation;
}
This the method I use in the view controller to maintain the orientation of the capture layer so that it is always right-side-up:
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
if (_previewLayer.connection.supportsVideoOrientation)
{
switch (toInterfaceOrientation)
{
case UIInterfaceOrientationPortrait:
{
_previewLayer.connection.videoOrientation = AVCaptureVideoOrientationPortrait;
break;
}
case UIInterfaceOrientationPortraitUpsideDown:
{
_previewLayer.connection.videoOrientation = AVCaptureVideoOrientationPortraitUpsideDown;
break;
}
case UIInterfaceOrientationLandscapeLeft:
{
_previewLayer.connection.videoOrientation = AVCaptureVideoOrientationLandscapeLeft;
break;
}
case UIInterfaceOrientationLandscapeRight:
{
_previewLayer.connection.videoOrientation = AVCaptureVideoOrientationLandscapeRight;
break;
}
}
}
A little wordy, but safe even if either enumeration ever changes in the future.
I am still not sure what's causing the problem but I managed to fix it. Here is how I did it:
in viewDidLoad I am adding a layer:
CGRect layerRect = [[[self view] layer] bounds];
[[[self captureManager] previewLayer] setBounds:layerRect];
[[[self captureManager] previewLayer] setPosition:CGPointMake(CGRectGetMidX(layerRect),CGRectGetMidY(layerRect))];
[[[self view] layer] addSublayer:[[self captureManager] previewLayer]];
Then I am adding call to the rotateLayer method to didRotate
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation{
[self rotateLayer];
}
and finally the rotateLayer method looks like:
-(void)rotateLayer{
CALayer * stuckview = [[self captureManager] previewLayer];
CGRect layerRect = [[[self view] layer] bounds];
UIDeviceOrientation orientation =[[UIDevice currentDevice]orientation];
switch (orientation) {
case UIDeviceOrientationLandscapeLeft:
stuckview.affineTransform = CGAffineTransformMakeRotation(M_PI+ M_PI_2); // 270 degress
break;
case UIDeviceOrientationLandscapeRight:
stuckview.affineTransform = CGAffineTransformMakeRotation(M_PI_2); // 90 degrees
break;
case UIDeviceOrientationPortraitUpsideDown:
stuckview.affineTransform = CGAffineTransformMakeRotation(M_PI); // 180 degrees
break;
default:
stuckview.affineTransform = CGAffineTransformMakeRotation(0.0);
[stuckview setBounds:layerRect];
break;
}
[stuckview setPosition:CGPointMake(CGRectGetMidX(layerRect),CGRectGetMidY(layerRect))];
}
I still don't understand why it works in this way. If anyone can explain it will be great.
#Janusz Chudzynski here is the detailed explanation what rotateLayer method does
This method is created after examine how different orientation affect the previewLayer so creater has checked when orientation is in LandscapeLeft then it should be 270 degrees rotated to make it in correct position for that he has used
stuckview.affineTransform = CGAffineTransformMakeRotation(M_PI+ M_PI_2);
M_PI 3.14159265358979323846264338327950288 // pi = 180 degree
+
M_PI_2 1.57079632679489661923132169163975144 // pi/2 = 90 degree
// Total = 270 degree
so creater has noticed that if I will rotate previewLayer to 270 degrees when its in LandscapeLeft then it will be in correct position just like that he has rotate previewLayer for every rotation possible
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
previewLayer.orientation = toInterfaceOrientation;
}
I'm with #Siegfault, although I also found that when my view loads in landscape orientation on the iPad initially, the orientation is still in correct. To fix, I call that same delegate method in viewDidAppear: with the current interfaceOrientation:
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self willRotateToInterfaceOrientation:self.interfaceOrientation duration:0.0];
}
The answer using willRotateToUserInterfaceOrientation works fine, except that that method has been deprecated. So if you're able to use iOS 9, then here's the way to do it, in Swift:
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
let newOrientation = UIDevice.currentDevice().orientation
switch newOrientation {
case .LandscapeLeft:
self.capturePreviewLayer?.connection.videoOrientation = .LandscapeLeft
case .LandscapeRight:
self.capturePreviewLayer?.connection.videoOrientation = .LandscapeRight
case .Portrait, .Unknown, .FaceUp, .FaceDown:
self.capturePreviewLayer?.connection.videoOrientation = .Portrait
case .PortraitUpsideDown:
self.capturePreviewLayer?.connection.videoOrientation = .PortraitUpsideDown
}
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
}
// layout iOS 8+ animated
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context)
{
NSString *timingFunc = nil;
switch ( [context completionCurve] )
{
case UIViewAnimationCurveEaseIn: timingFunc = kCAMediaTimingFunctionEaseIn; break;
case UIViewAnimationCurveEaseInOut: timingFunc = kCAMediaTimingFunctionEaseInEaseOut; break;
case UIViewAnimationCurveEaseOut: timingFunc = kCAMediaTimingFunctionEaseOut; break;
case UIViewAnimationCurveLinear: timingFunc = kCAMediaTimingFunctionLinear; break;
}
[CATransaction begin];
[CATransaction setAnimationDuration:[context transitionDuration]];
[CATransaction setAnimationTimingFunction:[CAMediaTimingFunction functionWithName:timingFunc]];
[self updatePreviewLayer];
[CATransaction commit];
UIInterfaceOrientation toOrientation = [[UIApplication sharedApplication] statusBarOrientation];
// layout ui if needed
} completion:^(id<UIViewControllerTransitionCoordinatorContext> context)
{
}];
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
}
- (void)updatePreviewLayer
{
CGAffineTransform transform = CGAffineTransformIdentity;
switch ( UIDevice.currentDevice.orientation )
{
case UIDeviceOrientationLandscapeLeft:
transform = CGAffineTransformRotate(transform, -M_PI_2);
break;
case UIDeviceOrientationLandscapeRight:
transform = CGAffineTransformRotate(transform, M_PI_2);
break;
case UIDeviceOrientationPortraitUpsideDown:
transform = CGAffineTransformRotate(transform, M_PI);
break;
default:
break;
}
preview.affineTransform = transform;
preview.frame = self.view.bounds;
}

Resources