Setting NavigationController.Viewcontrollers crashing (with custom transitions) - ios

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?

Related

Slow presentViewController performance

I am using UIViewControllerTransitioningDelegate to build custom transitions between two view controllers (from a MKMapView) to a custom Camera built on (AVFoundation). Everything goes well until I call the presentViewController and the phone seems to hang for about 1 second (when I log everything out). This even seems to happen when I am transitioning to a much simpler view (I have a view controller that only displays a UITextview and even with that there appears to be about a .4 - .5 second delay before the transition is actually called).
This is currently how I am calling the transition
dispatch_async(dispatch_get_main_queue(), ^{
UIStoryboard* sb = [UIStoryboard storyboardWithName:#"Main" bundle:nil];
CameraViewController *cvc2 = [sb instantiateViewControllerWithIdentifier:#"Camera"];
cvc2.modalPresentationStyle = UIModalPresentationFullScreen; // Needed for custom animations to work
cvc2.transitioningDelegate = self; //Conforms to the UIViewControllerTransitioningDelegate protocol
[self presentViewController:cvc2 animated:YES completion:nil];
});
Here is my animateTransition method for that call. Very straight forward and currently the view that is presenting this only has a MkMapView on it (no additional views or methods).
-(void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
if (self.type == MapAnimationTypePresent) {//From map to another view
UIView *containerView = [transitionContext containerView];
UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
// Amazing category for iOS 7 compatibility found here - http://stackoverflow.com/a/25193675/2939977
UIView *toView = [toViewController viewForTransitionContext:transitionContext];
UIView *fromView = [fromViewController viewForTransitionContext:transitionContext];
toView.frame = self.view.frame;
fromView.frame = self.view.frame;
//Add 'to' view to the hierarchy
toView.alpha = 0;
[containerView insertSubview:toView aboveSubview:fromView];
[UIView animateWithDuration:.5 animations:^{
toView.alpha = 1;
}completion:^(BOOL finished) {
[transitionContext completeTransition:YES];
}];
}
Any help is greatly appreciated.

Unbalanced calls to begin/end appearance transitions for <UIViewController: 0x176c0bd0> within a Navigation Controller

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];
}];
}

Container View Controller cannot handle Unwind Segue Action

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.

Modal View Disappears when using Custom transition

I'm trying to build a custom transition in iOS 7. The transition occurs fine, but then when transition context complete transition the modal screen disappears from the view entirely. I've followed several tutorials and I don't see what I am doing wrong. In addition, if I don't call "complete transition" then the view stays, but will not receive any touch events. I checked in Reveal App and there is no view sitting on top of it. Any ideas?
Here is the method where I initiate the transition
- (IBAction)settingsButtonClicked:(id)sender
{
UINavigationController *navigationController =[[self storyboard] instantiateViewControllerWithIdentifier:#"SettingsNavigationViewController"];
navigationController.transitioningDelegate = self;
[self presentViewController:navigationController animated:YES completion:nil];
}
Here is the code for the custom transition:
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *containerView = [transitionContext containerView];
[containerView addSubview:toViewController.view];
CGRect sourceRect = [transitionContext initialFrameForViewController:fromViewController];
CGRect initialTargetFrame = [transitionContext initialFrameForViewController:toViewController];
CGRect initialFrame = CGRectMake(sourceRect.size.width + initialTargetFrame.size.width, 0, initialTargetFrame.size.width, initialTargetFrame.size.height);
CGPoint destinationPoint = CGPointMake(sourceRect.size.width - 500, 0);
CGAffineTransform translate = CGAffineTransformMakeTranslation(initialFrame.origin.x, initialFrame.origin.y);
toViewController.view.transform = translate;
[UIView animateWithDuration:PRESENT_DURATION delay:0 options:UIViewAnimationOptionCurveEaseOut
animations:^{
toViewController.view.transform = CGAffineTransformMakeTranslation(destinationPoint.x, destinationPoint.y);
}
completion:^(BOOL completed) {
if (completed) {
[transitionContext completeTransition:!transitionContext.transitionWasCancelled];
}
}];
}
So I found out what I was doing wrong. I was changing the modalPresentationStyle to custom on the view controller that was being popped onto the navigation controller when I should have been setting it on the navigation controller itself. I added this line to the settingsButtonClicked method above and it worked properly.
navigationController.modalPresentationStyle = UIModalPresentationCustom;

When to call addChildViewController

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.

Resources