I'm currently working on a custom ViewController that manages at least one and up to five ViewController. They are aligned in a "plus", where the mandatory view controller is the center ViewController, and the other four can go on any side of the centre ViewController. The class is TDSlidingViewController in this project.
The problem I'm running into regards what view controllers are being passed into animateTransition. The following is the code preparing to transition (it's inside TDSlidingViewController.m):
targetViewController.transitioningDelegate = self;
targetViewController.modalPresentationStyle = UIModalPresentationNone;
if (_currentViewController == [self.slidingControllerDatasource viewControllerForLocation:SlidingViewCenter]) {
[_currentViewController presentViewController:targetViewController animated:YES completion:^(void) {
NSLog(#"%#\n%#\n%#", self.childViewControllers, self.view.subviews, self.currentViewController.childViewControllers);
[self.currentViewController.view addGestureRecognizer:[self gestureRecognizersForSlidingViewLocation:self.currentLocation][0]];
}];
} else {
[[self.slidingControllerDatasource viewControllerForLocation:self.previousLocation] dismissViewControllerAnimated:YES completion:^{}];
}
The following code runs in animateTransition:
UIView *container = transitionContext.containerView;
UIViewController *currentViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *targetViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
NSLog(#"%#", [currentViewController class]);
What caught my attention is that NSLog is not printing the _currentViewController, but the container for the SlidingViewController, TDMainViewController; the transition removes the entire stack of ViewController and displays one ViewController.
I believe it is causing [[self.slidingControllerDatasource viewControllerForLocation:self.previousLocation] dismissViewControllerAnimated:YES completion:^{}]; to crash with the following output (TDEditorViewController is the controller that was displayed from the transition initially):
*** Terminating app due to uncaught exception 'UIViewControllerHierarchyInconsistency', reason: 'presentedViewController for controller is itself on dismiss for: <`TDEditorViewController`: 0x8d69250>'
I have searched and searched, and the only link I can find that is similar is this, which has not been answered.
Any help would be greatly, greatly appreciated.
So I researched a little bit more, and came to the conclusion that the flaw was in the design. Instead of trying to present view controllers inside of a container view, I simply passed the gestures around so it knows when to move back to the center. The StackViewController that was originally going to be the container view, now facilitates the transitions from the background, holding the gesture recognizers, managing animations and passing around data. All that's changed is it's not displayed.
Related
I am working on an iPhone app using Storyboard and I need to handle view changes from one view controller to another one. I have:
-INTROViewController.m
-INTROscene.m (this is a SKScene laid out by the above controller)
-UpgradeViewController.m
There is a sprite button in INTROscene.m and when I press it, it triggers a notification which is seen by its view controller (INTROViewController.m) and this triggers the switch to another view controller (UpgradeViewController.m). If I use Option 1, (which even adds a delay in order to make sure that the first view has appeared), it triggers the error below:
“Attempt to present ViewController whose view is not in the window hierarchy!”
I’ve found a way of switching view controllers without triggering this error (Option 2) but the visual effect is horrible, with a little lag showing an empty screen between the two views. Moreover I cannot use any of the nice transitions which are available using modalTransitionStyle.
What is the correct way of switching between views in this situation?
In my AppDelegate I don’t have a root view controller (and I don’t know how this should be set up). Is that the reason why I get the error? If so, how could I implement it? Cheers!
//Option 1 (triggers the error above)
-(void)TransitionTo_Upgrades_ViewController:(NSNotification *)notification
{
//Take from INTROViewController to UpgradeViewController
UpgradeViewController *controllerUPGRADES = [self.storyboard instantiateViewControllerWithIdentifier:#"Upgrades_storyboard"];
controllerUPGRADES.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal;
[self performSelector:#selector(NowGotoUPRADES) withObject:nil afterDelay:2.0];
}
- (void)NowGotoUPRADES
{
[self presentViewController:controllerUPGRADES animated:YES completion:nil];
}
//Option 2 (no error but horrible effect)
-(void)TransitionTo_OPTIONS_ViewController:(NSNotification *)notification
{
[self performSelector:#selector(NowGotoOPTIONS) withObject:nil afterDelay:2.0];
}
- (void)NowGotoOPTIONS
{
//Take from INTROViewController to UpgradeViewController
UIStoryboard *storyBoard = [UIStoryboard storyboardWithName:#"Main_iPhone" bundle:nil];
UIViewController *DesiredViewController = [storyBoard instantiateViewControllerWithIdentifier:#"Options_storyboard"];
[[[[UIApplication sharedApplication] delegate] window] setRootViewController:DesiredViewController];
}
I have experienced this same problem and have found that there is no solution as once you move from scene to scene in a view controller, you are unable to move to other view controllers as you have moved out of the proper hierarchy. My solution to this was to have my other view controllers as skscenes, however you may want to have most of your skscenes as view controllers whilst also not moving between scenes whilst on the same view controller.
Hope this helps. Reply if you need any clarification.
I want to show a view only one time when a main view controller is shown for the first time.
-(void) viewDidAppear:(BOOL)animated
{
if (!self.isMainViewShowedBefore)
{
self. self.isMainViewShowedBefore = YES;
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController: temporaryViewController];
[self presentViewController:navigationController animated:NO completion:NULL];
}
}
It works but the main screen appears for a short time before the temporary view controller is shown.
I tried to add above code in viewWillAppear but I got below error
'NSInternalInconsistencyException', reason: 'Attempting to begin a
modal transition from to
while a transition is already in
progress. Wait for viewDidAppear/viewDidDisappear to know the current
transition has completed'
How can I hide the main controller before the temporary view controller is shown?
Add temporaryViewController.view as a subview of self.view (your main view controller)
[self.view addSubview:temporaryViewController.view];
You got the error in viewWillAppear because you can't do two animations together in iOS, it might get your app crashed but most of the time iOS is nice to us, it just gives us a warning.
Now my main concern, why would you add this main view controller if you don't want to use it at all? and this is not flickering, this is a normal behaviour, if you want the UINavigationController to appear, simply sow it instead of this "main view controller".
If you have more hidden logic that would prevent you from doing what I said, please tell me.
I have an iOS app that has a connection to a server. If we get disconnected, I want to be able to dismiss the top view controllers to get back to a "connecting to server" view controller. The problem is that a disconnection can occur at any time, including during a transition between view controllers.
The view controller hierarchy is like so:
ConnectingToServerViewController
SignInViewController
MainAppViewController
Other view controllers
When a disconnection is detected I want the view hierarchy to collapse back to:
ConnectingToServerViewController
So when a disconnection is detected, this method is called on the ConnectingToServerViewController to dismiss anything that it has presented and go back to attempting to connect to server:
- (void)restartSession
{
if (self.presentedViewController) {
[self dismissViewControllerAnimated:NO completion:nil];
}
}
However, if I try to dismiss while a view transition is occurring, I get errors such as
*** Assertion failure in -[UIWindowController transition:fromViewController:toViewController:target:didEndSelector:], /SourceCache/UIKit/UIKit-2380.17/UIWindowController.m:211
attempt to dismiss modal view controller whose view does not currently appear. self = <YYYYYViewController: 0x2089c8a0> modalViewController = <XXXXXViewController: 0x208e6610>
attempt to dismiss modal view controller whose view does not currently appear. self = <WWWWWWViewController: 0x1fd9e990> modalViewController = <YYYYYViewController: 0x2089c8a0>
The first of which will crash the app, the second will just not dismiss anything and continue to show the current presented view controller.
Thoughts:
delays won't work since we don't know when to start the delay
is there a way to track when view transitions complete?
should all view controllers override willAppear, didAppear and alert the app when it is safe to dismiss?
perhaps instead of dismiss, I should just set a new root view controller?
I've made sure that all overridden view(will|did)(dis)?appear methods call the appropriate super method.
Any solution that requires all view controllers to override view(did|will)appear methods to track state sounds like it could cause issues if we forget to set the base class for a new view controller.
Do something like this. Try this out once,
UIViewController *controller = self.presentingViewController; //THIS LINE IS IMP
[self dismissViewControllerAnimated:YES
completion:^{
[controller presentViewController:adminViewController animated:YES completion:nil];
adminViewController.view.superview.frame = CGRectMake(1024/2 - 400, 768/2 - 280, 800 , 560);//it's important to do this after
[adminViewController release];
}];
One way that has worked for me is to assign a new view controller to the root view controller. That way, views in the old hierarchy can animate and transition to their hearts content while we have new controllers.
eg
- (void)restartSession
{
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"MainStoryboard" bundle:nil];
ConnectingToServerViewController *vc = [storyboard instantiateViewControllerWithIdentifier:#"ConnectingToServerViewController"];
vc.modalPresentationStyle = UIModalPresentationFullScreen;
[UIApplication sharedApplication].delegate.window.rootViewController = vc;
}
I'm not sure if I'm aware of all the downsides to this though. Perhaps the old view controllers will never get freed because of a dangling strong reference? We're no longer reusing ConnectingToServerViewController, we have to recreate that each time.
I based the code on what I saw in this answer for Managing and dismissing Multiple View Controllers in iOS.
It seems like you are trying to dismiss the view controller when it is not currently on screen. To check if it is on screen you could use:
if (self.presentedViewController.view.window)
{
[self dismissViewControllerAnimated:NO completion:nil];
}
else
{
self.presentedViewController = nil;
}
I will answer in order.
is there a way to track when view transitions complete?
You could try with the UINavigationControllerDelegate (if you are using one of those). Other approach could be using a custom animator.
should all view controllers override willAppear, didAppear and alert the app when it is safe to dismiss?
That's an option. You are free to do it if you want. Another option is not to do that. I think that container view controllers such as navigation controller has better approaches.
I should just set a new root view controller?
I would suggest to do the opposite. I would set the SignInViewController / MainAppViewController as the root flow, and present modally ConnectingToServerViewController on demand. In my opinion that's a healthier approach.
Hope it helps.
I'm using a custom segue which looks like this:
#implementation ModalPushSegue
- (void)perform {
UIViewController *fromController = self.sourceViewController;
UIViewController *toController = self.destinationViewController;
UIView *fromView = fromController.view;
UIView *toView = toController.view;
CGPoint centerStage = toView.centerStage;
toView.center = toView.rightStage;
[fromView.window addSubview:toView];
[fromController addChildViewController:toController];
[UIView transitionWithView:toView
duration:0.5 options:0
animations:^{
toView.center = centerStage;
}
completion:nil];
}
This works well in that the view is slide on from the right as expected and the controller is added to the controller hierarchy.
But later in the added controller I do this:
[self presentViewController:anotherController animated:YES completion:nil];
I would expect this to slide the new controller's view up the screen ala modal style. But what happens instead is the the new view doesn't appear. And when I later remove this controller, it's view flashes up and slides off the screen, leaving a black background instead of the view that was originally there.
I've been playing around with this for a while and if I change the code to
//[self presentViewController:oauthController animated:YES completion:nil];
[self.view addSubview:oauthController.view];
[self addChildViewController:oauthController];
Then the view appears as expected, although not resized.
My problem appears to be with the way that the segues setup the hierarchy vs the way that presentViewController does things. I've done lots of reading and searching but so far have not been able to get a clear picture of exactly what is going on.
I've also played around with using presentViewController in the segue but instead of laying the new view over the old one, the screen goes black and the new view then slides on.
Any help appreciated.
Set a Storyboard ID on your destination view controller (in the storyboard), then try the following code:
AnotherController *viewC = [self.storyboard instantiateViewControllerWithIdentifier:#"AnotherController"];
ModalPushSegue *segue = [[ZHCustomSegue alloc] initWithIdentifier:#"coolSegueName" source:self destination:viewC];
[segue perform];
I'm running into a problem that I'm hoping someone here can help with.
I'm writing an app that includes ViewController containment. The main controller swaps the various child controllers in and out as the user manipulates a SegmentedController.
It seems to work correctly, but I've found a vulnerability. If I select segments TOO QUICKLY, I can get the app to crash with the following error:
2012-01-19 04:29:39.539 MyApp[1057:fb03] * Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Children view controllers (ChildViewController1): 0x6e91480 and ChildViewController1: 0x6e8dca0 must have a common parent view controller when calling -[UIViewController transitionFromViewController:toViewController:duration:options:animations:completion:]'
It SEEMS that the issue is that I'm triggering an action while my animation is running (and the child controllers are being swapped in and out), and that that's the problem, but I'm not exactly sure how to PROTECT the UI from doing this while the animation is running. Any ideas would be greatly appreciated. Here's the code that's running the VC swap:
- (IBAction)selectPage:(id)sender {
NSLog(#"Page Selected");
UIViewController *newViewController = [[self patientChartViewControllers] objectAtIndex:[sender selectedSegmentIndex]];
[self addChildViewController:newViewController];
[[self navigationItem] setTitle:[newViewController title]];
[self transitionFromViewController:[self currentPatientChartViewController] toViewController:newViewController duration:0.50 options:UIViewAnimationOptionTransitionCrossDissolve
animations:^{
[[[self currentPatientChartViewController] view] removeFromSuperview];
newViewController.view.frame = self.view.bounds;
[[self view] addSubview:[newViewController view]];
}
completion:^(BOOL finished){
[newViewController didMoveToParentViewController:self];
[[self currentPatientChartViewController] removeFromParentViewController];
[self setCurrentPatientChartViewController:newViewController];
}];
}
I am not quite sure, but I think the transition among view controllers is handled by Core Animation framework and for some reason, Core Animation doesn't like doing multiple things while interacting with user -- all the same time.
To prevent this from happening, my understading is that, your SegmentedController (presumably an instance of UISegmentControl) stays in the view, while views are being exchanged. For the time that view are being exchanged, you can disable the user interaction of SegmentController so that users can not switch views-- they have to wait for the complete transition of view A to B.
Hope that helps.
I've tried using transitionFromViewController:toViewController:duration:options:animations:completion: with the flag check using completion block, but it seems completion block finishes before the animation, even if I set duration to 0.
If user is changing child view controller really fast, I think we should add and remove child view controller ourself, so as to avoid the animation. This works for me
Something like this
// Removing currentVC
[currentVC willMoveToParentViewController:nil];
[currentVC.view removeFromSuperview];
[currentVC removeFromParentViewController];
// Adding nextVC
[self addChildViewController:nextVC];
nextVC.view.frame = self.view.bounds;
[self.view addSubview:nextVC.view];
[nextVC didMoveToParentViewController:self];
self.currentVC = nextVC