I am trying to update my willAnimateRotationToInterfaceOrientation code to use the new viewWillTransitionToSize as a part of IOS 9.
In my app, I have two totally separate views for landscape and portrait (they are laid out differently, in a different pattern).
During rotation the self.view of the viewcontroller is assigned to the correct view (landscape or portrait). In IOS 9 using viewWillTransitionToSize, this revealed a timing race condition between the rotation animation and the assignment of the self.view.
The assignment of self.view = portraitView or self.view = landscapeView could happen either before the rotation, or after the rotation. It appears that because a view assignment is not an "animatable" property that it is executed at any time during the animateWithDuration window.
How do I force the view assignment to happen before the rotation animation?
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator
{
UIInterfaceOrientation oldOrientation = [[UIApplication sharedApplication] statusBarOrientation];
UIInterfaceOrientation orientation = [Utilities orientationByTransforming:[coordinator targetTransform] fromOrientation:oldOrientation];
[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context)
{
[UIView animateWithDuration:[context transitionDuration] animations:^{
if (UIInterfaceOrientationIsLandscape(orientation)) {
self.view = self.landscapeView;
}
else{
self.view = self.portraitView;
}
} completion:^(BOOL finished){
//no completion logic for animateWithDuration
}];
} completion:^(id<UIViewControllerTransitionCoordinatorContext> context)
{
//no completion logic for animateAlongsideTransition
}];
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
}
I think you need to have both views available:
[self.view addSubview: self.landscapeView];
[self.view addSubview: self.portraitView];
And then fade between them
self.landscapeView.alpha = self.portraitView.alpha = 0.0f;
[UIView animateWithDuration:[context transitionDuration] animations:^{
if (UIInterfaceOrientationIsLandscape(orientation)) {
self.landscapeView.alpha = 1.0f;
}
else{
self.portraitView.alpha = 1.0f;
}
Related
I'm using the iOS 7 UIviewControllerAnimatedTransitioning protocol to present a modal ViewController with a custom animation. The animation works correctly, however, I want the newly presented ViewController to have a different status bar style than the presenting VC.
What I'm seeing is that -(UIStatusBarStyle)preferredStatusBarStyle gets called on the PRESENTING ViewController (several times in fact) and never on the newly presented ViewController. If I remove the custom animation everything with the status bar works as I'd expect.
Is there something special I need to do in my animateTransition function to update the root view controller or something? I've tried manually setting the statusBar with [UIApplication sharedApplication] setStatusBarStyle but it doesn't work (I think because I'm using the ios 7 view controller based status bar styling).
This is my code for animateTransition:
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
UICollectionViewCell *activeCell;
if ([self.collectionView.visibleCells containsObject:self.cellForActiveIdeaVC]) {
activeCell = self.cellForActiveIdeaVC;
}
UIView *container = transitionContext.containerView;
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *fromView = fromVC.view;
UIView *toView = toVC.view;
CGRect beginFrame;
if (activeCell) {
beginFrame = [container convertRect:activeCell.bounds fromView:activeCell];
} else {
beginFrame = CGRectMake(container.width / 2, container.height / 2, 0, 0);
}
CGRect endFrame = [transitionContext initialFrameForViewController:fromVC];
UIView *move = nil;
if (toVC.isBeingPresented) {
toView.frame = endFrame;
move = [toView snapshotViewAfterScreenUpdates:YES];
move.frame = beginFrame;
} else {
if (activeCell) {
move = [activeCell snapshotViewAfterScreenUpdates:YES];
} else {
move = [fromView snapshotViewAfterScreenUpdates:YES];
}
move.frame = fromView.frame;
[fromView removeFromSuperview];
}
[container addSubview:move];
[UIView animateWithDuration:.5
delay:0
usingSpringWithDamping:700
initialSpringVelocity:15
options:0
animations:^{
move.frame = toVC.isBeingPresented ? endFrame : beginFrame;
}
completion:^(BOOL finished) {
[move removeFromSuperview];
if (toVC.isBeingPresented) {
toView.frame = endFrame;
[container addSubview:toView];
} else {
if (self.cellForActiveIdeaVC) {
self.cellForActiveIdeaVC = nil;
}
}
[transitionContext completeTransition:YES];
}];
}
Any pointers much appreciated!
With iOS 7 custom transitions, it's possible to present a view controller that isn't fullscreen and therefore wouldn't affect the statusbar appearance. You have to explicitly tell iOS that your custom presented view controller will, in fact, control the status bar's appearance.
UIViewController *controllerToPresent = [...]
controllerToPresent.modalPresentationStyle = UIModalPresentationStyleCustom;
controllerToPresent.modalPresentationCapturesStatusBarAppearance = YES;
[self presentViewController:controllerToPresent animated:YES completion:nil];
There's some more information here. Hope that helps!
This worked for me:
[UIView animateWithDuration:0.25
delay:0
options:UIViewAnimationOptionCurveEaseOut
animations:^{
_preferredBarStyle = UIStatusBarStyleLightContent;
[self setNeedsStatusBarAppearanceUpdate];
}];
And then you just have to return this value on the preferredStatusBarStyle method:
- (UIStatusBarStyle) preferredStatusBarStyle {
return _preferredBarStyle;
}
I hope it helps!
I have a view controller with multiple child view controllers in it (set up using Storyboards), and I move the one on top to the right (with the status bar as well) to display the underlying sidebar view controller.
This works perfectly with frames, as shown below:
- (void)displaySidebar {
self.fullScreenSnapshotOverlay = [self takeFullScreenSnapshot];
[self.postsView addSubview:self.fullScreenSnapshotOverlay];
[self hideStatusBar];
[UIView animateWithDuration:0.4 animations:^{
CGRect newFrame = self.postsView.frame;
newFrame.origin.x += 200.0;
self.postsView.frame = newFrame;
}];
}
(hideStatusBar simply called the UIApplication method and layoutIfNeeded.)
Giving me this (perfect) result:
However, if in the Storyboard I go to the container view controller and make a constraint from its leading space to the left of the view controller it's embedded in, and then adjust that constant, it really messes up the navigation bar, I assume due to hiding the status bar and taking a screenshot. I'm using this code:
- (void)displaySidebar {
self.fullScreenSnapshotOverlay = [self takeFullScreenSnapshot];
[self.postsViewController.view addSubview:self.fullScreenSnapshotOverlay];
[self hideStatusBar];
[UIView animateWithDuration:0.4 animations:^{
self.postsViewControllerDistanceFromLeftSideConstraint.constant = 270.0;
[self.view layoutIfNeeded];
}];
}
Giving me this messed up result:
Now I know the simple thing to do would be to just continue with frames, but I'd like to learn how do it properly with Auto Layout. What am I doing wrong here?
I'm not sure how what I did is any different from what you're doing. I modified the code I posted to your other question (Why does hiding my status bar completely break my simple animation?) to what's below, and it worked fine.
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self performSelector:#selector(displaySidebar) withObject:nil afterDelay:1];
}
-(void)displaySidebar {
self.snapshotView = [self takeSnapshot];
[self.PostsView addSubview:self.snapshotView];
[self hideStatusBar];
self.leftCon.constant = 270;
[UIView animateWithDuration:1.0 animations:^{
[self.view layoutIfNeeded];
}];
}
-(void)moveOutMenu { // called from a button in the menu controller
self.leftCon.constant = 0;
[UIView animateWithDuration:1.0 animations:^{
[self.view layoutIfNeeded];
} completion:^(BOOL finished) {
[[UIApplication sharedApplication] setStatusBarHidden:NO];
[self.snapshotView performSelector:#selector(removeFromSuperview) withObject:nil afterDelay:0.01];
}];
}
-(UIView *)takeSnapshot {
UIView *v = [[UIScreen mainScreen] snapshotViewAfterScreenUpdates:NO];
return v;
}
-(void)hideStatusBar {
[[UIApplication sharedApplication] setStatusBarHidden:YES];
[self.view layoutIfNeeded];
}
What are all the constraints you have on the container view that you're moving? Maybe there's something different there (I have top, leading, bottom and width constraints on mine -- leftCon is the outlet to the leading constraint).
This is both a question and a partial solution.
*Sample project here:
https://github.com/JosephLin/TransitionTest
Problem 1:
When using transitionFromViewController:..., layouts done by the toViewController's viewWillAppear: doesn't show up when the transition animation begins. In other words, the pre-layout view shows during the animation, and it's contents snap to the post-layout positions after the animation.
Problem 2:
If I customize the background of my navbar's UIBarButtonItem, the bar button shows up with the wrong size/position before the animation, and snaps to the correct size/position when the animation ends, similar to Problem 1.
To demonstrate the problem, I made a bare-bone custom container controller that does some custom view transitions. It's pretty much a UINavigationController copy that does cross-dissolve instead of push animation between views.
The 'Push' method looks like this:
- (void)pushController:(UIViewController *)toViewController
{
UIViewController *fromViewController = [self.childViewControllers lastObject];
[self addChildViewController:toViewController];
toViewController.view.frame = self.view.bounds;
NSLog(#"Before transitionFromViewController:");
[self transitionFromViewController:fromViewController
toViewController:toViewController
duration:0.5
options:UIViewAnimationOptionTransitionCrossDissolve
animations:^{}
completion:^(BOOL finished) {
[toViewController didMoveToParentViewController:self];
}];
}
Now, DetailViewController (the view controller I'm pushing to) needs to layout its content in viewWillAppear:. It can't do it in viewDidLoad because it wouldn't have the correct frame at that time.
For demonstration purpose, DetailViewController sets its label to different locations and colors in viewDidLoad, viewWillAppear, and viewDidAppear:
- (void)viewDidLoad
{
[super viewDidLoad];
NSLog(#"%s", __PRETTY_FUNCTION__);
CGRect rect = self.descriptionLabel.frame;
rect.origin.y = 50;
self.descriptionLabel.frame = rect;
self.descriptionLabel.text = #"viewDidLoad";
self.descriptionLabel.backgroundColor = [UIColor redColor];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
NSLog(#"%s", __PRETTY_FUNCTION__);
CGRect rect = self.descriptionLabel.frame;
rect.origin.y = 200;
self.descriptionLabel.frame = rect;
self.descriptionLabel.text = #"viewWillAppear";
self.descriptionLabel.backgroundColor = [UIColor yellowColor];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
NSLog(#"%s", __PRETTY_FUNCTION__);
CGRect rect = self.descriptionLabel.frame;
rect.origin.y = 350;
self.descriptionLabel.frame = rect;
self.descriptionLabel.text = #"viewDidAppear";
self.descriptionLabel.backgroundColor = [UIColor greenColor];
}
Now, when pushing the DetailViewController, I'm expecting to see the label at y =200 at the begining of the animation (left image), and then jumps to y = 350 after the animation is finished (right image).
Expected view before and after animation.
However, the label was at y=50, as if the layout made in viewWillAppear didn't make it before the animation took place (left image). But notice that the label's background was set to yellow (the color specified by viewWillAppear)!
Wrong layout at the beginning of the animation. Notice that the bar buttons also start with the wrong position/size.
Console Log
TransitionTest[49795:c07] -[DetailViewController viewDidLoad]
TransitionTest[49795:c07] Before transitionFromViewController:
TransitionTest[49795:c07] -[DetailViewController viewWillAppear:]
TransitionTest[49795:c07] -[DetailViewController viewWillLayoutSubviews]
TransitionTest[49795:c07] -[DetailViewController viewDidLayoutSubviews]
TransitionTest[49795:c07] -[DetailViewController viewDidAppear:]
Notice that viewWillAppear: was called AFTER transitionFromViewController:
Solution for Problem 1
Alright, here comes the partial solution part. By explicitly calling beginAppearanceTransition: and endAppearanceTransition to toViewController, the view will have the correct layout before the transition animation takes place:
- (void)pushController:(UIViewController *)toViewController
{
UIViewController *fromViewController = [self.childViewControllers lastObject];
[self addChildViewController:toViewController];
toViewController.view.frame = self.view.bounds;
[toViewController beginAppearanceTransition:YES animated:NO];
NSLog(#"Before transitionFromViewController:");
[self transitionFromViewController:fromViewController
toViewController:toViewController
duration:0.5
options:UIViewAnimationOptionTransitionCrossDissolve
animations:^{}
completion:^(BOOL finished) {
[toViewController didMoveToParentViewController:self];
[toViewController endAppearanceTransition];
}];
}
Notice that viewWillAppear: is now called BEFORE transitionFromViewController:
TransitionTest[18398:c07] -[DetailViewController viewDidLoad]
TransitionTest[18398:c07] -[DetailViewController viewWillAppear:]
TransitionTest[18398:c07] Before transitionFromViewController:
TransitionTest[18398:c07] -[DetailViewController viewWillLayoutSubviews]
TransitionTest[18398:c07] -[DetailViewController viewDidLayoutSubviews]
TransitionTest[18398:c07] -[DetailViewController viewDidAppear:]
But that doesn't fix Problem 2!
For whatever reason, the navbar buttons still begin with the wrong position/size at the beginning of the transition animation. I spent so many time trying to find THE right solution but without luck. I'm starting to feel it's a bug in transitionFromViewController: or UIAppearance or whatever. Please, any insight you can offer to this question is greatly appreciated. Thanks!
Other solutions I've tried
Call [self.view addSubview:toViewController.view]; before transitionFromViewController:
It actually gives exactly the right result to the user, fixes both Problem 1&2. The problem is, viewWillAppear and viewDidAppear will both be called twice! It's problematic if I want to do some expansive animation or calculation in viewDidAppear.
Call [toViewController viewWillAppear:YES]; before transitionFromViewController:
I think it's pretty much the same as calling beginAppearanceTransition:. It fixes Problem 1 but not Problem 2. Plus, the doc says not to call viewWillAppear directly!
Use [UIView animateWithDuration:] instead of transitionFromViewController:
Like this:
[self addChildViewController:toViewController];
[self.view addSubview:toViewController.view];
toViewController.view.alpha = 0.0;
[UIView animateWithDuration:0.5 animations:^{
toViewController.view.alpha = 1.0;
} completion:^(BOOL finished) {
[toViewController didMoveToParentViewController:self];
}];
It fixes Problem 2, but the view started with the layout in viewDidAppear (label is green, y=350). Also, the cross-dissolve is not as good as using UIViewAnimationOptionTransitionCrossDissolve
Ok, adding layoutIfNeeded to the toViewController.view seems to do the trick - this gets the view laid out properly before it shows up on screen (without the add/remove), and no more weird double viewDidAppear: call.
- (void)pushController:(UIViewController *)toViewController
{
UIViewController *fromViewController = [self.childViewControllers lastObject];
[self addChildViewController:toViewController];
toViewController.view.frame = self.view.bounds;
[toViewController.view layoutIfNeeded];
NSLog(#"Before transitionFromViewController:");
[self transitionFromViewController:fromViewController
toViewController:toViewController
duration:0.5
options:UIViewAnimationOptionTransitionCrossDissolve
animations:^{}
completion:^(BOOL finished) {
}];
}
Had the same problem, all you need is forwarding appearance transactions and UIView.Animate. This approach fixes all problems, doesn't create new ones. Here is some C# code (xamarin):
var fromView = fromViewController.View;
var toView = toViewController.View;
fromViewController.WillMoveToParentViewController(null);
AddChildViewController(toViewController);
fromViewController.BeginAppearanceTransition(false, true);
toViewController.BeginAppearanceTransition(true, true);
var frame = fromView.Frame;
frame.X = -viewWidth * direction;
toView.Frame = frame;
View.Add(toView);
UIView.Animate(0.3f,
animation: () =>
{
toView.Frame = fromView.Frame;
fromView.MoveTo(x: viewWidth * direction);
},
completion: () =>
{
fromView.RemoveFromSuperview();
fromViewController.EndAppearanceTransition();
toViewController.EndAppearanceTransition();
fromViewController.RemoveFromParentViewController();
toViewController.DidMoveToParentViewController(this);
}
);
and of course you should disable automatic forwarding of appearance methods and do it manually:
public override bool ShouldAutomaticallyForwardAppearanceMethods
{
get { return false; }
}
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
CurrentViewController.BeginAppearanceTransition(true, animated);
}
public override void ViewDidAppear(bool animated)
{
base.ViewDidAppear(animated);
CurrentViewController.EndAppearanceTransition();
}
public override void ViewWillDisappear(bool animated)
{
base.ViewWillDisappear(animated);
CurrentViewController.BeginAppearanceTransition(false, animated);
}
public override void ViewDidDisappear(bool animated)
{
base.ViewDidDisappear(animated);
CurrentViewController.EndAppearanceTransition();
}
I have an iPad project structured as:
- AppDelegate
- MainWindow
- View Controller
-- View
The View Controllers .m file loads another view programmatically and positions it on the screen. This view is going to be slid in and out.
I do this:
- (void)viewDidLoad
{
[super viewDidLoad];
CGRect viewRect = CGRectMake(0, 0, 0, 0);
CalculatorView *v = [[[CalculatorView alloc]
initWithFrame:viewRect] autorelease];
[self.view.window addSubview:v];
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationCurve:UIViewAnimationCurveEaseIn];
v.view.frame = CGRectMake(0, 0, 460, 320);
[UIView commitAnimations];
}
The issue that I am having is that the subview I'm adding here doesnt' seem to have the correct orientation. The project supports ONLY landscape, and launches to landscape. The container view is fine, and it contains some buttons which are fine as well. However this programmatically loaded view is stuck in Portrait mode. I have provided the following auto-rotation code (In the loaded view's .m):
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
// Return YES for supported orientations
return (interfaceOrientation == UIInterfaceOrientationLandscapeLeft || interfaceOrientation == UIInterfaceOrientationLandscapeRight);
}
But it never gets called.
So, how can I get the programmatically added subview to load in landscape and NOT portrait mode?
TIA!
The UIView class does not receive orientation change messages.
(specially the shouldAutorotateToInterfaceOrientation method, which is a UIViewController Method)
http://developer.apple.com/library/ios/#documentation/uikit/reference/UIViewController_Class/Reference/Reference.html#//apple_ref/doc/uid/TP40006926-CH3-SW23
You will have to manually add a method in your view to inform it that the orientation has changed and you should call this method in your controller shouldAutorotateToInterfaceOrientation method.
To do that you will have to create a reference to you view in your controller and take care of the memory yourself.
#interface MyController : UIViewController {
CalculatorView *_calculatorView;
}
#end
#implementation MyController
- (void)viewDidLoad
{
[super viewDidLoad];
CGRect viewRect = CGRectMake(0, 0, 0, 0);
//inits _calculatorView without the autorelease. Will be released in the dealloc method
_calculatorView = [[CalculatorView alloc]
initWithFrame:viewRect];
[self.view.window addSubview:v];
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationCurve:UIViewAnimationCurveEaseIn];
_calculatorView.view.frame = CGRectMake(0, 0, 460, 320);
[UIView commitAnimations];
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
//calls custom interface orientation method
[_calculatorView MyInterfaceChangedCustomMethod:interfaceOrientation];
// Return YES for supported orientations
return (interfaceOrientation == UIInterfaceOrientationLandscapeLeft || interfaceOrientation == UIInterfaceOrientationLandscapeRight);
}
-(void) dealloc {
[_calculatorView release];
[super dealloc];
}
#end
EDIT: If you CalculatorView is simple and all you need is to change its frame properly after the device rotation, I think the best approach would be using you view's autoresizingMask similar to the following
_calculatorView = [[CalculatorView alloc]
initWithFrame:viewRect];
_calculatorView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
In order to use a UISplitViewController, I'm replacing my window root controller when navigating from one view controller to the other.
In order to have some nice transition while doing so, I'm using a zooming effect like this:
MyOtherViewController *controller = [[MyOtherViewController alloc] initWithNibName:#"MyOtherView" bundle:nil];
UIWindow *window = ((MyAppDelegate *)[[UIApplication sharedApplication] delegate]).window;
controller.view.frame = [window frame];
controller.view.transform = CGAffineTransformMakeScale(0.01,0.01);
controller.view.alpha = 0;
[window addSubview:controller.view];
[UIView animateWithDuration:0.2 animations:^{
controller.view.transform = CGAffineTransformMakeScale(1,1);
controller.view.alpha = 1.0;
} completion:^(BOOL finished) {
if (finished) {
[self.view removeFromSuperview];
window.rootViewController = controller;
}
}];
and this works pretty well, except that while doing the animation, the new view is always oriented as if in portrait mode, regardless of the current device orientation. When the animation is finished, the view orients itself correctly.
What am I missing?
Things I've tried:
putting my new controller view as the sole subview of the UIWindow
making my new controller the root view controller before the animation begins
A curious thing is that, if I do a recursiveDescription on the window at the beginning of my method, the window frame is defined as having a size of 768x1024 (i.e., portrait), and the view inside it of 748x1024 but with a transform of [0, -1, 1, 0, 0, 0] (does this mean a rotation or what? Shouldn't it be the identity transform?)
UIWindow doesn't rotate. It has a rotated view inside of it (as you've seen). In this case, though, I think the problem is likely that your view has a transform on it already at this point, and you need to concatenate with it rather than replace it as you're doing in your setTransform: calls.
You shouldn't be asking the app delegate for the window, you should be getting the window from the view (self.view.window).
If at any point you're attaching your view to the window itself, rather than putting it inside the rotation view, then you'll need to know the effective transform of the view you want to match by walking the hierarchy:
- (CGAffineTransform)effectiveTransform {
CGAffineTransform transform = [self transform];
UIView *view = [self superview];
while (view) {
transform = CGAffineTransformConcat(transform, [view transform]);
view = [view superview];
}
return transform;
}
I finally figured out what was wrong. Since the frame is not a real property but a sort of calculated one, based on the values of the view bounds and the view transform, I needed to set the frame after setting the same transform as the current view, and before setting the transform again to set up the initial state of the animation. Also, the frame I need to set is the same one as the current view is currently using, as it is taking into account the window orientation (or lack thereof, as Rob Napier pointed)
So, without further ado, here's the working code:
MyOtherViewController *controller = [[MyOtherViewController alloc] initWithNibName:#"MyOtherView" bundle:nil];
UIWindow *window = [[UIApplication sharedApplication] keyWindow];
CGAffineTransform t = self.view.transform;
controller.view.transform = t;
controller.view.frame = self.view.frame;
controller.view.transform = CGAffineTransformScale(t,.01,.01);;
[window addSubview:controller.view];
controller.view.alpha = 0;
[UIView animateWithDuration:0.2 animations:^{
controller.view.transform = t;
controller.view.alpha = 1.0;
} completion:^(BOOL finished) {
if (finished) {
[self.view removeFromSuperview];
window.rootViewController = controller;
[controller release];
}
}];