Starting in iOS 11, UIViewController's transitionFromViewController:toViewController:duration:options:animations:completion: method appears to not call its completion block anymore.
Sample code snippet below:
[self addChildViewController:toVC];
[fromVC willMoveToParentViewController:nil];
[self transitionFromViewController:fromVC
toViewController:toVC
duration:0.4
options:UIViewAnimationOptionTransitionCrossDissolve
animations:^{}
completion:^(BOOL finished) {
NSLog(#"Completion called"); // this completion is never executed
}];
This is causing me all kinds of issues in getting my views to transition and animate correctly. Has anyone else run into this behavior, and/or discovered a workaround?
So it turns out that I wasn't explicitly adding toVC.view as a subview to self.view after adding toVC as a child view controller to self. Odd that this behaves differently in iOS 11 vs. previous versions, but this did the trick:
[self addChildViewController:toVC];
[self.view addSubview:toVC.view]; // This line is what is needed
[fromVC willMoveToParentViewController:nil];
[self transitionFromViewController:fromVC
toViewController:toVC
duration:0.4
options:UIViewAnimationOptionTransitionCrossDissolve
animations:^{}
completion:^(BOOL finished) {
NSLog(#"Completion called");
}];
Related
I am using the following code to override the perform method in a custom segue to achieve sliding effect when moving between view controllers.
- (void)perform
{
MasterController *sourceController = (MasterController *)self.sourceViewController;
MasterController *destinationController = (MasterController *)self.destinationViewController;
CGRect frame = sourceController.view.frame;
[sourceController.view addSubview:destinationController.view];
[destinationController.view setFrame:CGRectOffset(frame, frame.size.width, 0)];
[UIView animateWithDuration:0.5 animations:^{
[sourceController.view setFrame:CGRectOffset(frame, -frame.size.width, 0)];
} completion:^(BOOL finished) {
[sourceController presentViewController:destinationController animated:NO completion:nil];
[destinationController.view removeFromSuperview];
}];
}
Similar to this code exists all over the internet. the problem is that "sometimes" after the animation finish the screen flashes/blinks/flickers then gets back normal.
removing [destinationController.view removeFromSuperview]; line of code seems to solve the problem. but, that doesn't look right! right?
Any ideas how to solve this?
Yes, simply remove [destinationController.view removeFromSuperview];. It will be done for you. At the end of the segue, destinationController.view will have a new superview and will be removed from sourceController.view.
I'm trying to create an unwind segue with the page curl animation. I've followed a tutorial I found only to have the destination view controller call segueForUnwindingToViewController to return an instance of my custom segue class. However, at best it just instantly changes to the appropriate view without a delay or any animation.
Any help would be appreciated.
The contents of perform:
[UIView animateWithDuration:0.5
delay:0.0
options:UIViewAnimationOptionTransitionCurlUp
animations:^{}
completion:^(BOOL finished){
[destinationViewController.view removeFromSuperview];
[sourceViewController dismissViewControllerAnimated:NO completion:NULL];
}];
I am trying to override removeFromSuperview for a custom view i created. The view is intended to contain a UIActivityIndicatorView and i managed to animate its appearance successfully overriding the initWithFrame method. Now i would like to override also its removeFromSuperview adding in it a fade out animation so that from the view controller i can simply call [self.loadingView removeFromSuperview]; Unfortunately the loadingView is removed without the animation, probably because of the [super removeFromSuperview]; i need to call when overriding. Here is the code i'm using, is there a way i can fix it?
-(void)removeFromSuperview
{
[super removeFromSuperview];
[UIView animateWithDuration:0.5
animations:^{self.alpha = 0.0;}
completion:^(BOOL finished){[self removeFromSuperview];}];
}
Try putting the [super removeFromSuperview] in the completion block for the animation:
-(void)removeFromSuperview
{
[UIView animateWithDuration:0.5
animations:^{self.alpha = 0.0;}
completion:^(BOOL finished){[super removeFromSuperview];}];
}
This is how I am using the containment API. According to the docs is its correct.
[self.childViewController willMoveToParentViewController:nil];
[UIView animateWithDuration:0.25 animations:^{
self.childViewController.view.frame = ... // View Animation
} completion:^(BOOL finished) {
[self.childViewController.view removeFromSuperview]; // triggers `viewWillDisappear`
[self.childViewController removeFromParentViewController];
}];
I expect viewWillDisappear to be called before the animation begins and viewDidDisappear to be called after the animation completes. However, they are both called in quick succession after the animation is complete.
Moving [self.childViewController.view removeFromSuperview]; to the animation block fixes this, but the code looks wrong:
[self.childViewController willMoveToParentViewController:nil];
[UIView animateWithDuration:0.25 animations:^{
self.childViewController.view.frame = ... // View Animation
[self.childViewController.view removeFromSuperview]; // triggers `viewWillDisappear`
} completion:^(BOOL finished) {
[self.childViewController removeFromParentViewController];
}];
Does anyone know what the proper way to get viewWillDisappear to be called at the correct time is?
The answer was to use – beginAppearanceTransition:animated: & endAppearanceTransition
If you are implementing a custom container controller, use this method to tell the child that its views are about to appear or disappear. Do not invoke viewWillAppear:, viewWillDisappear:, viewDidAppear:, or viewDidDisappear: directly.
The corrected code:
[self.childViewController willMoveToParentViewController:nil];
[self.childViewController beginAppearanceTransition:NO animated:YES];
[UIView animateWithDuration:0.25 animations:^{
self.childViewController.view.frame = ...
} completion:^(BOOL finished) {
[self.childViewController.view removeFromSuperview];
[self.childViewController removeFromParentViewController];
[self.childViewController endAppearanceTransition];
}];
You are not notifying that the childViewController moved to the parentViewController.
[yourViewController didMoveToParentViewController:self]
And in [self.childViewController willMoveToParentViewController:nil]; You are not mentioning who will be the parentViewController. It is a good practice to indicate when the childViewController was added. Then iOS should know when to trigger viewWillDisappear
You are notifying that a viewController will move to a parentViewController but you are not indicating who is the parentViewController if you indicate which the parentViewController is your code will work as expected.
ViewWillDissappear will get fired when the view is ready to leave the parentView. This will happens in the next willLayoutSubviews execution of your parentViewController or viewController depending in the hierarchy of your viewControllers
[UIView animateWithDuration:0.25 animations:^{
[self.childViewController.view removeFromSuperview];
self.childViewController.view.frame = ... // View Animation
[self.view addSubView:self.childViewController.view];
} completion:^(BOOL finished) {
[self.childViewController didMoveToParentViewController:self];
// triggers `viewWillDisappear`
[self.childViewController removeFromParentViewController];
}];
Try that
Here is a Swift version.
It place beginAppearanceTransition in animation closure so you get similar behavior like UINavigationController: You can adjust animatable view properties in child view controller's viewWillDisappear and get animation automatically.
let animator = UIViewPropertyAnimator(duration: 0.5, dampingRatio: 1) {
childViewController.willMove(toParentViewController: self)
childViewController.beginAppearanceTransition(false, animated: true)
childViewController.view.frame = ... // View Animation
}
animator.addCompletion { _ in
childViewController.view.removeFromSuperview()
childViewController.endAppearanceTransition()
childViewController.removeFromParentViewController()
}
animator.startAnimation()
Notice the endAppearanceTransition must be called after view.removeFromSuperview otherwise system will send duplicate viewWillDisappear viewDidDisappear methods to your child view controller.
How is it possible that I change the transition for presenting a modal view controller. Is it possible that the presenting transition is using the default UIModalTransitionStyle....
UIModalTransitionStyleCoverVertical = 0,
UIModalTransitionStyleFlipHorizontal,
UIModalTransitionStyleCrossDissolve,
UIModalTransitionStylePartialCurl,
but the dismiss transition is using the UIViewAnimationTransitionCurlUp transition. Important is that I don't want to use the UIModalTransitionStylePartialCurl it should be the CurlUp one.
Sadly the following code doesn't work:
[UIView transitionFromView:self.view toView:self.parentViewController.parentViewController.parentViewController.view duration:1.0 options:UIViewAnimationTransitionCurlUp completion:^(BOOL finished) {....}];
Maybe it has something to do that the view controller is displayed in modal mode.
It would be nice if someone can help.
It feels kludgy, but you can do this by animating the transition of adding your destination view controller's view, but on the completion of that, you immediately remove it again and then properly transition to it via the presentViewController (this time without animation, since the visual effect has already been rendered).
I do this in a subclassed custom UIStoryboardSegue (you didn't say NIBs or storyboard, but the concept is the same):
- (void)perform
{
UIViewController *src = self.sourceViewController;
UIViewController *dst = self.destinationViewController;
[UIView transitionWithView:src.navigationController.view
duration:0.75
options:UIViewAnimationOptionTransitionCurlUp | UIViewAnimationOptionCurveEaseInOut
animations:^{
[src.navigationController.view addSubview:dst.view];
}
completion:^(BOOL finished){
[dst.view removeFromSuperview];
[src presentViewController:dst animated:NO completion:nil];
}];
}
Clearly, if your source view controller doesn't have a navigation controller, you would replace those "src.navigationController.view" with just "src.view". But hopefully this gives you the idea.
And, anticipating the logical follow-up question, when dismissing the view controller, I have a button hooked up to an IBAction:
- (IBAction)doneButton:(id)sender
{
[UIView transitionWithView:self.view.superview
duration:0.75
options:UIViewAnimationOptionTransitionCurlDown
animations:^{
[self dismissViewControllerAnimated:NO completion:nil];
}
completion:nil];
}