I'm implementing a custom segue using controller containment API, e.g.
#implementation CustomSegue
- (void)perform {
UIViewController *sourceViewController = self.sourceViewController;
UIViewController *destinationViewController = self.destinationViewController;
[sourceViewController addChildViewController:destinationViewController];
destinationViewController.view.alpha = 0.0;
[sourceViewController.view addSubview:destinationViewController.view];
[UIView animateWithDuration:0.3 animations:^{
destinationViewController.view.alpha = 1.0;
} completion:^(BOOL finished) {
[destinationViewController didMoveToParentViewController:sourceViewController];
}];
}
#end
View controller hierarchy is trivial: sourceViewController → destinationViewController
When unwinding from the destinationViewController to the sourceViewController, app crashes in [UIStoryboardUnwindSegueTemplate _perform:] with exception Could not find a view controller to execute unwinding for <…>
I did not implement custom -[UIViewController viewControllerForUnwindSegueAction:fromViewController:withSender: or
-[UIViewController canPerformUnwindSegueAction:fromViewController:withSender: which means framework returns correct values (although I implemented it once to check).
When replacing my custom addChildViewController:… code with presentViewController:… in the segue, it works fine: unwinding performs like expected.
The question: is it possible to have a custom segue that creates a custom view controller hierarchy?
Test case project: https://bitbucket.org/zats/unwind/
I think that George Green's comment is relevant, and the way you have your controllers set up is the cause of the crash. I think to do what you want, you should have ZTSFirstViewController added as a child to a custom container controller which will do the unwinding, and the (forward) segue will exchange the children of that custom container controller (switch from ZTSFirstViewController to ZTSSecondViewController). The viewControllerForUnwindSegueAction:fromViewController:withSender: method needs to be implemented in the custom container controller (ViewController in my example). I tested this by adding a container view in IB to ViewController, and changed the class of the embedded controller to ZTSFirstViewController. I added a segue from a button in ZTSFirstViewController to ZTSSecondViewController, and connected the unwind segue from a button in that controller. The unwind: method is in ZTSFirstViewController. The code in the custom segue was this,
- (void)perform {
UIViewController *destinationViewController = self.destinationViewController;
UIViewController *sourceViewController = self.sourceViewController;
if (!self.unwind) {
[sourceViewController.parentViewController addChildViewController:destinationViewController];
destinationViewController.view.alpha = 0.0;
[sourceViewController.parentViewController.view addSubview:destinationViewController.view];
[UIView animateWithDuration:0.3 animations:^{
destinationViewController.view.alpha = 1.0;
} completion:^(BOOL finished) {
[destinationViewController didMoveToParentViewController:sourceViewController.parentViewController];
}];
} else {
[self.sourceViewController willMoveToParentViewController:nil];
[UIView animateWithDuration:0.3 animations:^{
sourceViewController.view.alpha = 0.0;
} completion:^(BOOL finished) {
[sourceViewController.view removeFromSuperview];
[sourceViewController removeFromParentViewController];
}];
}
}
I kept this segue close to your implementation -- it doesn't actually switch the controllers, it just adds the second one as a second child and hides the view of the first.
Related
I have written a custom segue, because i wanted to add my own animations to it. Everything works alright, besides the fact that the viewDidLoad method in the target view CiewController gets called twice. Here is the perform method for my segue:
- (void)perform
{
UIViewController* sourceViewController = self.sourceViewController;
UIViewController* destinationViewController = self.destinationViewController;
[sourceViewController.view addSubview:destinationViewController.view];
CGPoint originalCenter = destinationViewController.view.center;
destinationViewController.view.center = CGPointMake(self.originatingPoint.x * 3, self.originatingPoint.y);
[UIView animateWithDuration:0.25
delay:0.0
options:UIViewAnimationOptionCurveEaseInOut
animations:^{
destinationViewController.view.center = originalCenter;
}
completion:^(BOOL finished) {
[sourceViewController presentViewController:destinationViewController animated:NO completion:NULL]; // present VC
}];
}
Does anyone have any idea what could cause this?
[----- EDIT -----]
The segue is present in the storyboard as a custom segue with a segue class that I have written myself. The only thing that is different in my class is the perform method which I have put above. The segue is called through a button, and the prepareForSegue method is called only once.
[----- EDIT 2 -----]
I checked the viewDidLoad method of the targetVC and it is only called once per segue. Nonentheless, it would be much more convenient for me to use viewWillAppear, so do you maybe know a different way in which i can do this animation?
I suggest you to use UIViewControllerTransitioningDelegate and UIViewControllerAnimatedTransitioning which are the more appropriate conventions to use for transitions since iOS 7.0. Since iOS 8.0 you also gain UIPresentationController support which allows you to build even richer transitions.
Example:
#interface ModalTransitionAnimator : NSObject<UIViewControllerAnimatedTransitioning>
#property (nonatomic) CGPoint originatingPoint;
#end
#implementation ModalTransitionAnimator
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext {
return 0.25;
}
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
NSTimeInterval duration = [self transitionDuration:transitionContext];
UIView* sourceView = [transitionContext viewForKey:UITransitionContextFromViewKey];
UIView* destinationView = [transitionContext viewForKey:UITransitionContextToViewKey];
UIView* container = transitionContext.containerView;
[container addSubview:destinationView];
CGPoint originalCenter = destinationView.center;
destinationView.center = CGPointMake(self.originatingPoint.x * 3, self.originatingPoint.y);
[UIView animateWithDuration:duration animations:^{
destinationView.center = originalCenter;
}
completion:^(BOOL finished) {
[transitionContext completeTransition:YES];
}];
}
#end
Then in prepareForSegue you simply assign transitioning delegate and implement UIViewControllerTransitioningDelegate to return appropriate animators for presentation or dismissal.
#interface ViewController : UIViewController<UIViewControllerTransitioningDelegate>
#end
#implementation ViewController
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
UIViewController* controller = (UIViewController*)segue.destinationViewController;
controller.transitioningDelegate = self;
controller.modalPresentationStyle = UIModalPresentationCustom;
controller.modalPresentationCapturesStatusBarAppearance = YES;
}
- (id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
ModalTransitionAnimator *animator = [[ModalTransitionAnimator alloc] init];
animator.originatingPoint = /* ... */;
return animator;
}
#end
Since this is a modal transition, you have to use presentViewController:animated: when presenting controllers with it. Therefore use normal "show" segues in Storyboards and they will automatically run all animations under the hood, no need to reinvent segues here.
I had example of how to build custom transitions somewhere on Github:
https://github.com/pronebird/CustomModalTransition/tree/ios8
When I am creating a custom segue from one view controller to another, there's a problem with showing top bar properly.
This is the code for custom segue:
#implementation CustomSegue
- (void)perform {
UIViewController *sourceViewController = self.sourceViewController;
UIViewController *destinationViewController = self.destinationViewController;
[sourceViewController.view addSubview:destinationViewController.view];
destinationViewController.view.alpha = 0.0;
[UIView animateWithDuration:0.5
delay:0.0
options:UIViewAnimationOptionCurveEaseInOut
animations:^{
destinationViewController.view.alpha = 1.0;
}
completion:^(BOOL finished){
[sourceViewController presentViewController:destinationViewController animated:NO completion:NULL];
[destinationViewController.view removeFromSuperview];
}];
}
#end
I am using storyboard to set this up and sourceViewCotoller is embeded in NavigationViewController.
After performing a segue, there is no top bar in th destinationViewController.
I found an answer that I should add a Navigation Item to the destinationViewController - doesn't help.
Another way of dealing with this that I found is embedding the destinationViewController in NavigationViewController.
After doing this, the top bar is visible, but also during the segue.
It's probably because I am adding the desitnationViewController into sourceViewController, so at the beginning of segue I have 2 top bars visible one below another.
How to deal with this ?
I am building an application using UISplitViewController as my root view controller (as prescribed by Apple). However, I needed a custom view for login / management to be displayed prior to the UISplitViewController, so I created a custom UIStoryboardSegue that calls some custom animations. I am attempting to recreate the push / pop segues through a modal segue, without actually pushing an popping view controllers. I've implemented everything correctly, however, at the end of my animation I have a flicker. Here is a gif of it:
Here is my custom Segue's code:
- (void)perform {
UIViewController *srcViewController = (UIViewController *) self.sourceViewController;
UIViewController *destViewController = self.destinationViewController;
UIView *prevView = srcViewController.view;
UIView *destView = destViewController.view;
UIWindow *window = [[[UIApplication sharedApplication] delegate] window];
[window insertSubview:destView aboveSubview:prevView];
[destView enterRight:0.1 then:^{
[destView removeFromSuperview];
[srcViewController.presentingViewController dismissViewControllerAnimated: NO completion:nil];
}];
}
And here is my custom animation (Implemented as a category on UIView):
-(void)enterRight:(float)delay then:(void(^)(void))after
{
CGPoint moveTo = self.center;
CGPoint moveFrom = self.center;
// Grab a point from off the screen
CGFloat simpleOffscreen = [UIScreen mainScreen].bounds.size.width;
// come from off the right side (+)
moveFrom.x = moveFrom.x + simpleOffscreen;
self.center = moveFrom;
self.hidden = NO;
[UIView animateWithDuration:0.5
delay:delay
usingSpringWithDamping:1
initialSpringVelocity:0.1
options:UIViewAnimationOptionCurveEaseIn
animations:^
{
self.center = moveTo;
}
completion:^(BOOL finished)
{
if (after) after();
}
];
}
As you can see in the Segue, I am animating the view into the current view controller, then without animation presenting the actual destination view controller. I think this is where the flicker is introduced, yet I am unsure about how to go about preventing this.
My Storyboard for this custom segue is
Anyone know how to implement this?
I am using a custom unwind segue in a navigation controller, in the animation of the segue the navigation bar is not visible during the animation, when the animation ends the navigation bar 'pops'. ¿How can i retain the visibility of the navigation bar during the animation?
More details:
I have a button in the navigation bar that calls modal view this animation performs as expected, the new view has a button to trigger the unwind segue animation the view to grow and disappear, while this animation is performing the Navigation Bar in the destination view controller is not visible until the animation is finished.
This is the code i'm using for the custom segue.
- (void) perform {
UIViewController *sourceViewcontroller = self.sourceViewController;
UIViewController *destinationViewcontroller = self.destinationViewController;
[sourceViewcontroller.view.superview insertSubview:destinationViewcontroller.view atIndex:0];
[destinoViewcontroller beginAppearanceTransition:YES animated:YES];
[UIView animateWithDuration:0.2
delay:0.0
options:UIViewAnimationOptionCurveEaseInOut
animations:^{
origenViewcontroller.view.transform = CGAffineTransformMakeScale(1.5, 1.5);
origenViewcontroller.view.alpha = 0.0;
}
completion:^(BOOL finished){
[destinationViewcontroller.view removeFromSuperview];
[sourceViewcontroller dismissViewControllerAnimated:NO completion:NULL];
}];
}
Ok, so i think i got it, what i did was inserting the whole navigation controller view in the superview of the source view and removing the code to remove the destination view from the superview and setting to YES the option of dismissViewControllerAnimated like this:
- (void) perform {
UIViewController *origenViewcontroller = self.sourceViewController;
UIViewController *destinoViewcontroller = self.destinationViewController;
[origenViewcontroller.view.superview insertSubview:destinoViewcontroller.navigationController.view atIndex:0];
[UIView animateWithDuration:0.4
delay:0.0
options:UIViewAnimationOptionCurveEaseInOut
animations:^{
origenViewcontroller.view.transform = CGAffineTransformMakeScale(2.0, 2.0);
origenViewcontroller.view.alpha = 0.0;
}
completion:^(BOOL finished){
[origenViewcontroller dismissViewControllerAnimated:YES completion:NULL];
}];
}
I'm still not sure if this is the correct way to do it.
You could embed the destinationViewController in a UINavigationController too and set your segue from the sourceViewController to the Navigation 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];
}