I am facing a problem while trying to unwind using a custom segue from a view controller that was added as a child to another view controller.
Here is MyCustomSegue.m:
- (void)perform
{
if (_isPresenting)
{
//Present
FirstVC *fromVC = self.sourceViewController;
SecondVC *toVC = self.destinationViewController;
toVC.view.alpha = 0;
[fromVC addChildViewController:toVC];
[fromVC.view addSubview:toVC.view];
[UIView animateWithDuration:1.0 animations:^{
toVC.view.alpha = 1;
//fromVC.view.alpha = 0;
} completion:^(BOOL finished){
[toVC didMoveToParentViewController:fromVC];
}];
}
else
{
//Dismiss
}
}
And here is my FirstVC.m:
- (void)prepareForSegue:(MyCustomSegue *)segue sender:(id)sender
{
segue.isPresenting = YES;
}
- (UIViewController *)viewControllerForUnwindSegueAction:(SEL)action fromViewController:(UIViewController *)fromViewController withSender:(id)sender
{
return self;
}
- (UIStoryboardSegue *)segueForUnwindingToViewController:(UIViewController *)toViewController fromViewController:(UIViewController *)fromViewController identifier:(NSString *)identifier
{
return [[MyCustomSegue alloc] initWithIdentifier:identifier source:fromViewController destination:toViewController];
}
- (IBAction)unwindToFirstVC:(UIStoryboardSegue *)segue
{
NSLog(#"I am here");
}
All the necessary connections are done in the storyboard as well.
My problem is that -segueForUnwindingToViewController: is never called.
As soon as -viewControllerForUnwindSegueAction:fromViewController:withSender: is returned, my program crashes with the following exception:
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Could not find a view controller to execute unwinding for <FirstViewController: 0x8e8d560>'
As of my understanding, the reason for the crash, is that I want my container view controller to be the one to handle the unwind segue action, which is not possible (because container view controller asks only its children to handle the unwind segue.
Is it correct?
What can I do to solve my problem?
Thanks!
I don't think you can use an unwind segue that way (at least I couldn't find a way). Instead, you can create another "normal" segue from the child controller back to the parent, and set its type to custom, with the same class as you used to go from the first controller to the second. In prepareForSegue in the second controller, set isPresenting to NO, and have this code in your custom segue,
- (void)perform {
if (_isPresenting) {
NSLog(#"Presenting");
ViewController *fromVC = self.sourceViewController;
SecondVC *toVC = self.destinationViewController;
toVC.view.alpha = 0;
[fromVC addChildViewController:toVC];
[fromVC.view addSubview:toVC.view];
[UIView animateWithDuration:1.0 animations:^{
toVC.view.alpha = 1;
} completion:^(BOOL finished){
[toVC didMoveToParentViewController:fromVC];
}];
}else{
NSLog(#"dismissing");
SecondVC *fromVC = self.sourceViewController;
[fromVC willMoveToParentViewController:nil];
[UIView animateWithDuration:1.0 animations:^{
fromVC.view.alpha = 0;
} completion:^(BOOL finished) {
[fromVC removeFromParentViewController];
}];
}
}
If you want to go back from SecondVC to FirstVC, then try using this code where you are telling your controller to go back to previous controller.
[self dismissViewControllerAnimated:YES completion:nil];
This will work and you won't need unwinding code.
Hope this Helps.
Related
So this is my viewcontroller hierarchy:
-RootVC
-MapVC
-VC0
-VC1
-VC2
The app starts of by pushing #[Root, MapVC] to the view controller -- All good.
Then, my goal is to have the Map loaded (therefore, I don't want to pop the MapVC) while changing between the four main VCs (MapVC, VC0, VC1, VC2).
To do this, I use the following code:
-(void)presentViewController:(NSString*)viewControllerIdentifier{
UIViewController *vc = [self.storyboard instantiateViewControllerWithIdentifier:viewControllerIdentifier];
if (![[self.navigationController.viewControllers objectAtIndex:[[self.navigationController viewControllers] count]-1] isKindOfClass:[vc class]]){
NSMutableArray *viewControllers = [NSMutableArray arrayWithArray:[[self navigationController] viewControllers]];
if (![[viewControllers lastObject] isKindOfClass:[LLMapViewController class]]) {
[viewControllers removeLastObject];
}
[viewControllers addObject:vc];
[[self navigationController] setViewControllers:viewControllers animated:YES];
}
}
-(void)presentViewController0:(NSNotification*)notif{
[self presentViewController:[notif name]];
}
--
Method to present the map:
-(void)presentMapViewController:(NSNotification*)notif{
if (![[self.navigationController.viewControllers objectAtIndex:[[self.navigationController viewControllers] count]-1] isKindOfClass:[LLMapViewController class]]){
[self.navigationController popViewControllerAnimated:YES];
}
}
Well, it works well if I don't animate this transitions.
Also, when I do the following actions everything works well
App Launches:
->Root
->MapVC
ACTIONS:
0: Select VC0
->Root
->MapVC
->VC0
1: Select VC1 (Pops VC0 and pushes VC1, as you can see in the code above)
->Root
->MapVC
->VC1
2: Select MapVC (Pops VC1)
->Root
->MapVC
The problem is when I try to push another VC having the MapVC as main VC:
3: Select VC0
->Root
->Map
....
CRASH!
It doesn't show any consistent error.
Sometimes it doesn't show any error and sometimes it shows errors like these:
Example 1:
-[NSLayoutConstraint navigationController:animationControllerForOperation:fromViewController:toViewController:]:
Example 2:
-[UILabel navigationController:animationControllerForOperation:fromViewController:toViewController:]:
But it always highlights the part when I am setting the navigationcontroller's viewcontrollers .
MY PROBLEM: I have spent around 8 hours trying to fix this. I have no more ideas on how to debug this.
Can anyone please help me?
P.S. I am using custom transitions to push/pop the controllers.
Here's additional code:
-(id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC {
LLMenuAnimation *animationController = [[LLMenuAnimation alloc] initWithNavigationController:self.navigationController];
switch (operation) {
case UINavigationControllerOperationPush:
animationController.type = AnimationTypePush;
return animationController;
case UINavigationControllerOperationPop:
animationController.type = AnimationTypePop;
return animationController;
default:
NSLog(#"OTHER OPERATION");
return nil;
}
}
--
#pragma mark - Animated Transitioning
-(NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext {
return 0.2;
}
-(void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
//Get references to the view hierarchy
UIView *containerView = [transitionContext containerView];
UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
if (self.type == AnimationTypePush) {
//Add 'to' view to the hierarchy with 0.5 scale
toViewController.view.transform = CGAffineTransformMakeScale(0.5, 0.5);
[containerView insertSubview:toViewController.view aboveSubview:fromViewController.view];
//Scale the 'to' view to to its final position
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
// toViewController.view.frame = fromViewController.view.frame;
toViewController.view.transform = CGAffineTransformMakeScale(1.0, 1.0);
} completion:^(BOOL finished) {
[transitionContext completeTransition:YES];
}];
} else if (self.type == AnimationTypePop) {
//Add 'to' view to the hierarchy
[containerView insertSubview:toViewController.view belowSubview:fromViewController.view];
//Scale the 'from' view down
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
fromViewController.view.transform = CGAffineTransformMakeScale(0.5, 0.5);
} completion:^(BOOL finished) {
[transitionContext completeTransition:YES];
}];
}
}
I don't believe that you can use UINavigationController the way you are trying to do. If I have understood what you are trying to do, you are wanting to replace the navigationController's viewControllers property from underneath the navigation controller.
By the way, viewControllers property displayed is a copy. This is the way it is defined in the documentation:
#property(nonatomic, copy) NSArray *viewControllers
Your code:
NSMutableArray *viewControllers = [NSMutableArray arrayWithArray:[[self navigationController] viewControllers]];
if (![[viewControllers lastObject] isKindOfClass:[LLMapViewController class]]) {
[viewControllers removeLastObject];
}
[viewControllers addObject:vc];
[[self navigationController] setViewControllers:viewControllers animated:YES];
}
won't work the way you are expecting. You can push and pop view controllers onto the navigation controller. You can't replace the viewControllers the way you are trying to do.
By the way, the view controller hierarchy you drew in the first few lines of your question above is incomplete. Where is the navigation controller in the hierarchy?
I am trying to write a custom segue and have come across this error
Unbalanced calls to begin/end appearance transitions for UIViewController: 0x176c0bd0
The help button is connected to the almost empty ViewController - and the exit button unwinds the segue
All the controllers are embedded in a navigation Controller.
I've read through various posts here where people have encountered the same problem, but the solution varies a lot, and I still haven't found the right solution. I think it is because I am calling the custom segue from within a Navigation Controller, but that my code doesn't reflect that. I've followed this tutorial to create the custom segue http://blog.dadabeatnik.com/2013/10/13/custom-segues/
The initial controller has the following methods:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if([segue isKindOfClass:[ICIHelpSegue class]]) {
((ICIHelpSegue *)segue).originatingPoint = self.help.center;
}
}
- (IBAction)unwindFromViewController:(UIStoryboardSegue *)sender {
}
- (UIStoryboardSegue *)segueForUnwindingToViewController:(UIViewController *)toViewController fromViewController:(UIViewController *)fromViewController identifier:(NSString *)identifier {
ICIUnwindHelpSegue *segue = [[ICIUnwindHelpSegue alloc] initWithIdentifier:identifier source:fromViewController destination:toViewController];
segue.targetPoint = self.help.center;
return segue;
}
The ICIHelpSegue class is the following interface:
#interface ICIHelpSegue : UIStoryboardSegue
#property CGPoint originatingPoint;
#property CGPoint targetPoint;
#end
And the implementation file looks like this:
#implementation ICIHelpSegue
- (void)perform {
UIViewController *sourceViewController = self.sourceViewController;
UIViewController *destinationViewController = self.destinationViewController;
UINavigationController *navigationController = sourceViewController.navigationController;
[navigationController.view addSubview:destinatiionViewController.view]
// Transformation start scale
destinationViewController.view.transform = CGAffineTransformMakeScale(0.05, 0.05);
// Store original centre point of the destination view
CGPoint originalCenter = destinationViewController.view.center;
// Set center to start point of the button
destinationViewController.view.center = self.originatingPoint;
[UIView animateWithDuration:0.5
delay:0.0
options:UIViewAnimationOptionCurveEaseInOut
animations:^{
// Grow!
destinationViewController.view.transform = CGAffineTransformMakeScale(1.0, 1.0);
destinationViewController.view.center = originalCenter;
}
completion:^(BOOL finished){
[destinationViewController.view removeFromSuperview]; // remove from temp super view
[navigationController presentViewController:destinationViewController animated:NO completion:NULL]; // present VC
}];
}
#end
Any ideas why this error occurs? What it means? And how to solve it?
I have the same error. The issue appears to be related to the fact that removeFromSuperview is called in the same run loop as the call to presentViewController:animated:completion for the same viewController/view.
One thing you can do to get rid of this warning is to present the view controller after a delay:
completion:^(BOOL finished)
{
destinationViewController.view removeFromSuperview];
[navigationController performSelector:#selector(presentViewController:) withObject:destinationViewController afterDelay:0];
}];
}
-(void)presentViewController:(UIViewController*)viewController
{
[[self presentViewController:viewController animated:NO completion:nil];
}
However, both this method and the one with the warning will sometimes have a flicker because there is one frame where the view has been removed from the superview, but not presented yet.
To get around this issue, you can create a snapshot view of the destination view controller, and add that to the view hierarchy in place of the actual destinationViewController's view, and then remove it AFTER the destinationViewController has been presented:
completion:^(BOOL finished)
{
UIView * screenshotView = [destinationViewController.view snapshotViewAfterScreenUpdates:NO];
screenshotView.frame = destinationViewController.view.frame;
[destinationViewController.view.superview insertSubview:screenshotView aboveSubview: destinationViewController.view];
[destinationViewController.view removeFromSuperview];
[self performSelector:#selector(presentViewControllerRemoveView:) withObject:#[destinationViewController, screenshotView] afterDelay:0];
}];
}
-(void)presentViewControllerRemoveView:(NSArray *)objects
{
UIViewController * viewControllerToPresent = objects[0];
UIView * viewToRemove = objects[1];
[self presentViewController:viewControllerToPresent
animated:NO
completion:
^{
[viewToRemove removeFromSuperview];
}];
}
I'm developing a Share Extension for iOS 8 with custom UI, but it appears without animation, how can I do this? It's a regular UIViewController.
Also, it appears on fullscreen on iPad and I want it to be a modal view controller, that appears in the center of the screen and doesn't fit it, how can I do this?
Regards.
Here is the cleanest solution I found so far to animate my custom view controller in and out!
Animate IN:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
self.view.transform = CGAffineTransformMakeTranslation(0, self.view.frame.size.height);
[UIView animateWithDuration:0.25 animations:^
{
self.view.transform = CGAffineTransformIdentity;
}];
}
Dismiss:
- (void)dismiss
{
[UIView animateWithDuration:0.20 animations:^
{
self.view.transform = CGAffineTransformMakeTranslation(0, self.view.frame.size.height);
}
completion:^(BOOL finished)
{
[self.extensionContext completeRequestReturningItems:nil completionHandler:nil];
}];
}
Instead of animating the UIViewController's view, I am suggesting a bit different approach.
I have created a dummy UIViewController (called PresentingViewController here on) whose view.backgroundColor is set to [UIColor clearColor]. I then present the intended custom UIViewController modally (or custom animation if you like) on top.
This is the code for the PresentingViewController:
#implementation PresentingViewController
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self performSegueWithIdentifier:#"PresentController" sender:self];
}
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([segue.identifier isEqualToString:#"PresentController"]) {
CustomViewController *controller = (CustomViewController *)[segue.destinationViewController topViewController];
controller.context = self.extensionContext;
}
}
- (IBAction)unwindFromShareVC:(UIStoryboardSegue *)segue {
[self dismissViewControllerAnimated:YES completion:^{
NSError *error = [NSError errorWithDomain:#"Cancelled" code:0 userInfo:nil];
[self.extensionContext cancelRequestWithError:error];
}];
}
#end
Notes:
extensionContext is set only on the PresentingViewController and thus it is required to be passed on to the CustomViewController.
For animating dismiss, I was unable to use an unwind segue, because it was difficult to know the completion of dismissal. So I used dismissViewControllerAnimated:completion: instead.
I'm trying to have something similar to a UINavigationController so I can customize the animations. To start, I'm just using Apple stock animations. Here's my containerViewController:
- (void)loadView {
// Set up content view
CGRect frame = [[UIScreen mainScreen] bounds];
_containerView = [[UIView alloc] initWithFrame:frame];
_containerView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
self.view = _containerView;
}
- (id)initWithInitialViewController:(UIViewController *)vc {
self = [super init];
if (self) {
_currentViewController = vc;
[self addChildViewController:_currentViewController];
[self.view addSubview:_currentViewController.view];
[self didMoveToParentViewController:self];
_subViewControllers = [[NSMutableArray alloc] initWithObjects:_currentViewController, nil];
}
return self;
}
- (void)pushChildViewController:(UIViewController *)vc animation:(UIViewAnimationOptions)animation {
vc.view.frame = _containerView.frame;
[self addChildViewController:vc];
[self transitionFromViewController:_currentViewController toViewController:vc duration:0.3 options:animation animations:^{
}completion:^(BOOL finished) {
[self.view addSubview:vc.view];
[vc didMoveToParentViewController:self];
[self.subViewControllers addObject:vc];
}];
}
- (void)popChildViewController:(UIViewController *)vc WithAnimation:(UIViewAnimationOptions)animation {
// Check that there is a view controller to pop to
if ([self.subViewControllers count] <= 0) {
return;
}
NSInteger idx = [self.subViewControllers count] - 1;
UIViewController *toViewController = [_subViewControllers objectAtIndex:idx];
[vc willMoveToParentViewController:nil];
[self transitionFromViewController:vc toViewController:toViewController duration:0.3 options:animation animations:^{
}completion:^(BOOL finished) {
[vc.view removeFromSuperview];
[vc removeFromParentViewController];
[self didMoveToParentViewController:toViewController];
[self.subViewControllers removeObjectAtIndex:idx];
}];
}
I have this ContainerViewcontroller as my rootViewController of the window. I can add my initial viewController and push a view controller. When I try to pop though, I get
ContainerViewController[65240:c07] Unbalanced calls to begin/end appearance transitions for <SecondViewController: 0x8072130>.
I'm wondering what I am doing wrong. I figured my initialViewController is still underneath the secondViewController. Any thoughts? Thanks!
I don't know if this is what's causing your problem, but shouldn't this:
[self didMoveToParentViewController:toViewController];
be:
[toViewController didMoveToParentViewController:self];
Also, I'm not sure what you're doing with the subViewControllers array. It seems to be a duplication of the childViewControllers array that is already a property of a UIViewController.
One other thing I'm not sure is right. In your pop method your toViewController is the last controller in the _subViewControllers array. Don't you want it to be the second to last? Shouldn't the last be the one you're popping? You're popping vc, which is a controller you're passing in to the method, I don't understand that.
This is the way I've made a navigation like controller. In its containment behavior, it acts like a navigation controller, but without a navigation bar, and allows for different transition animations:
#implementation ViewController
-(id)initWithRootViewController:(UIViewController *) rootVC {
if (self = [super init]) {
[self addChildViewController:rootVC];
rootVC.view.frame = self.view.bounds;
[self.view addSubview:rootVC.view];
}
return self;
}
-(void)pushViewController:(UIViewController *) vc animation:(UIViewAnimationOptions)animation {
vc.view.frame = self.view.bounds;
[self addChildViewController:vc];
[self transitionFromViewController:self.childViewControllers[self.childViewControllers.count -2] toViewController:vc duration:1 options:animation animations:nil
completion:^(BOOL finished) {
[vc didMoveToParentViewController:self];
NSLog(#"%#",self.childViewControllers);
}];
}
-(void)popViewControllerAnimation:(UIViewAnimationOptions)animation {
[self transitionFromViewController:self.childViewControllers.lastObject toViewController:self.childViewControllers[self.childViewControllers.count -2] duration:1 options:animation animations:nil
completion:^(BOOL finished) {
[self.childViewControllers.lastObject removeFromParentViewController];
NSLog(#"%#",self.childViewControllers);
}];
}
-(void)popToRootControllerAnimation:(UIViewAnimationOptions)animation {
[self transitionFromViewController:self.childViewControllers.lastObject toViewController:self.childViewControllers[0] duration:1 options:animation animations:nil
completion:^(BOOL finished) {
for (int i = self.childViewControllers.count -1; i>0; i--) {
[self.childViewControllers[i] removeFromParentViewController];
}
NSLog(#"%#",self.childViewControllers);
}];
}
After Edit: I was able to duplicate the back button function with this controller by adding a navigation bar to all my controllers in IB (including in the one that is the custom container controller). I added a bar button to any controllers that will be pushed, and set their titles to nil (I got some glitches if I left the title as "item"). Deleting that title makes the button disappear (in IB) but you can still make connections to it in the scene list. I added an IBOutlet to it, and added this code to get the function I wanted:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
if (self.isMovingToParentViewController) {
self.backButton.title = [self.parentViewController.childViewControllers[self.parentViewController.childViewControllers.count -2] navigationItem].title;
}else{
self.backButton.title = [self.parentViewController.childViewControllers[self.parentViewController.childViewControllers.count -3] title];
}
}
I've shown two different ways that worked to access a title -- in IB you can set a title for the controller which I used in the else clause, or you can use the navigationItem title as I did in the if part of the clause. The "-3" in the else clause is necessary because at the time viewWillAppear is called, the controller that is being popped is still in the childViewControllers array.
addChildViewController should be called first
For adding / removing, you can refer to this great category and have no worry when to call it:
UIViewController + Container
- (void)containerAddChildViewController:(UIViewController *)childViewController {
[self addChildViewController:childViewController];
[self.view addSubview:childViewController.view];
[childViewController didMoveToParentViewController:self];
}
- (void)containerRemoveChildViewController:(UIViewController *)childViewController {
[childViewController willMoveToParentViewController:nil];
[childViewController.view removeFromSuperview];
[childViewController removeFromParentViewController];
}
In addition to rdelmar's answer you should not be calling addView/removeFromSuperview transitionFromViewController does this for you, from the documentation:
This method adds the second view controller’s view to the view
hierarchy and then performs the animations defined in your animations
block. After the animation completes, it removes the first view
controller’s view from the view hierarchy.
I'd like to replicate the behavior of the iPhone's Music app. When you're playing an album in that app and you tap the upper right button the album cover flips around to show a UITableView of tracks behind it.
Is it possible to accomplish this with a custom UIStoryboardSegue?
Or is the best way just to flip between two views that use the same controller?
It is probably simpler to flip between two views of the same view controller, e.g.
- (IBAction)showTracksView
{
[UIView transitionWithView:self.view
duration:1.0
options:UIViewAnimationOptionTransitionFlipFromLeft
animations:^{ tracksView.hidden = NO; }
completion:^(BOOL finished){ self.navigationItem.title = #"Tracks"; }];
}
- (IBAction)hideTracksView
{
[UIView transitionWithView:self.view
duration:1.0
options:UIViewAnimationOptionTransitionFlipFromLeft
animations:^{ tracksView.hidden = YES; }
completion:^(BOOL finished){ self.navigationItem.title = #"Album cover"; }];
}
where tracksView is your UITableView of tracks.
I had this challenge and solved it using a custom segue to present the view controller. Just create a new class based on UIStoryboardSegue.
Here is my custom segue
.h file:
#import <UIKit/UIKit.h>
#interface BRTrackNotesSegue : UIStoryboardSegue
#end
.m file
#implementation BRTrackNotesSegue
- (void) perform {
UIViewController *src = (UIViewController *) self.sourceViewController;
UIViewController *dst = (UIViewController *) self.destinationViewController;
[UIView transitionWithView:src.navigationController.view duration:0.50
options:UIViewAnimationOptionTransitionFlipFromLeft
animations:^{
[src.navigationController pushViewController:dst animated:NO];
}
completion:NULL];
}
#end
In the interface builder select the segue and set the Segue Class to the nam of your custom segue.
The second view controller contains the following to dismiss with the same animation :
- (IBAction)done:(id)sender {
[UIView transitionWithView:self.navigationController.view
duration:0.50
options:UIViewAnimationOptionTransitionFlipFromLeft
animations:nil
completion:nil];
[self.navigationController popViewControllerAnimated:NO];
}