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
Related
I had this code to make a flip transition between UITabControllers:
UIStoryboard *sb = [UIStoryboard storyboardWithName:#"OtherSb" bundle:nil];
PrimaryTabBarController *tabBarController = [sb instantiateInitialViewController];
[UIView transitionWithView:[APP_DELEGATE window]
duration:0.8
options:UIViewAnimationOptionTransitionFlipFromLeft
animations:^{
[[APP_DELEGATE window] setRootViewController:tabBarController];
[[APP_DELEGATE window] makeKeyAndVisible];
}
completion:nil];
However, strangely, during the flip transition, the tab bar briefly flashes from the bottom to the top of the screen. I was able to make that stop by doing the following:
PrimaryTabBarController *tabBarController = [sb instantiateInitialViewController];
tabBarController.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal;
[self presentViewController:tabBarController animated:YES completion:nil];
The problem with this however is that I'm pushing another view controller onto the stack which can easily run through the memory. How can I create a new navigation stack without having the tab bar mess up the animation?
rootViewController is not very animatable property as you can imagine. makeKeyAndVisible is not animatable either and you should probably do that before you run any animations.
This API itself is very old (iOS 4.0) and I personally consider it more of a legacy of UIKit and I haven't seen it being used since iOS 6 when flipping views was still a thing.
Custom transitions introduced in iOS 7 is a very comfortable way of doing any kind of crazy animations however as you noticed, it creates a modal hierarchy that you don't always need.
All of these API were designed to work with sibling views within container. This is something mentioned in documentation and its samples. And it seems like that UIWindow is not suitable as global animation container.
Sample code from documentation:
[UIView transitionWithView:containerView
duration:0.2
options:UIViewAnimationOptionTransitionFlipFromLeft
animations:^{ [fromView removeFromSuperview]; [containerView addSubview:toView]; }
completion:NULL];
I suggest that you follow the same logic as custom transitions and first setup dummy root controller that will serve you as a container for animation.
Then you add your views or entire view controllers inside of it and run animation between sibling views using
+ transitionFromView:toView:duration:options:completion:`
or
- transitionFromViewController:toViewController:duration:options:animations:completion:
or
+ transitionWithView:duration:options:animations:completion:
In addition to that, there is a useful flag UIViewAnimationOptionShowHideTransitionViews that will automatically hide the flipped view to avoid it to flicker or re-appear after animation.
When animation is over, you can swap the entire root controller in one call, that should be unnoticed to user.
This API has some quirks too, for example if by any chance you use it when the app is not on screen or you run it on a window that is not currently visible, then it will simply swallow the call. I used to have a check like
if(fromViewController.view.window) {
/* run animations */
} else {
/* swap controllers without animations */
}
I made a sample project to demonstrate how to use temporary container view for transition
https://github.com/pronebird/FlipRootController
Sample category on UIWindow:
#implementation UIWindow (Transitions)
- (void)transitionToRootController:(UIViewController *)newRootController animationOptions:(UIViewAnimationOptions)options {
// get references to controllers
UIViewController *fromVC = self.rootViewController;
UIViewController *toVC = newRootController;
// setup transition view
UIView *transitionView = [[UIView alloc] initWithFrame:self.bounds];
// add subviews into transition view
[transitionView addSubview:toVC.view];
[transitionView addSubview:fromVC.view];
// add transition view into window
[self addSubview:transitionView];
// flush any outstanding animations
// UIButton may cancel transition if this method is called from touchUpInside, etc..
[CATransaction flush];
[UIView transitionFromView:fromVC.view
toView:toVC.view
duration:0.5
options:options
completion:^(BOOL finished) {
// set new root controller after animation
self.rootViewController = toVC;
// move VC's view out of transition view
[self addSubview:toVC.view];
// remove transition view
[transitionView removeFromSuperview];
}];
}
#end
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.
I'm not using a navigation controller or a tab bar controller, I'm not using the push/pop method or presenting views modally. In my main view controller, I am adding a view controller like so:
UIViewController *nextController;
nextController = [[GamePlayViewController alloc] initWithNibName:#"GamePlayView" bundle:nil];
[nextController performSelector:#selector(setDelegate:) withObject:self];
temporaryController = nextController;
[self.view addSubview:nextController.view];
This view controller follows a delegate protocol and when the user is finished in this game view, this code is called:
[delegate backToMenu:self];
which calls this function in the app's main view controller:
- (void)backToMenu:(GamePlayViewController *)sender {
NSLog(#"back to menu");
[temporaryController.view removeFromSuperview];
}
Removing the view with removeFromSuperview seems to get rid of the view only, but I can see due to NSLogging that code is still executing in the .m file of that removed view. The view is still in in the app's memory. It has not been discarded as I had hoped.
"Release" is an old relic never to be used with ARC, so how can I entirely remove the viewController that was created with alloc/initWithNibName?
Thanks!
You should also be using the view controller life cycle methods.
Adding:
GamePlayViewController *nextController = [[GamePlayViewController alloc] initWithNibName:#"GamePlayView" bundle:nil];
nextController.delegate = self;
[self addChildViewController:nextController];
[self.view addSubview:nextController.view];
[nextController didMoveToParentViewController:self];
temporaryController = nextController;
Removing:
[temporaryController didMoveToParentViewController:nil];
[temporaryController.view removeFromSuperview];
[temporaryController removeFromParentViewController];
temporaryController = nil;
Also if temporaryController is a strong property (or you've used an iVar), you should nil it out after removing it.
As the CAAnimation retains its delegate make you remove the animation and nil out the delegate.
-(void)didMoveToParentViewController:(UIViewController *)parentViewController
{
[super didMoveToParentViewController:parentViewController];
if (!parentViewController) {
CAAnimation *animation = [movingObject.layer animationForKey:#"animatePositionX"];
animation.delegate = nil;
[movingObject.layer removeAnimationForKey:#"animatePositionX"];
}
}
If you want to check your view controller is being deallocoated you should implement the dealloc method and place a breakpoint inside of it. I suggest a breakpoint over a NSLog as I don't know how much you already log out so it might get missed, with a breakpoint it is much clearly - actually stopping the program flow.
I am struggling with understanding why the first method below works for hiding and removing a subview of a view. In this first method I pass the pointer by reference. In the second method, which is less general, I have a delegate method designed for removing a specific view. I would like to use the first method, because I have several views that I would like to apply this too. I should mention that the first method works without fail as long as it is called within the implementing class. It fails when I call it from the view controller that I wish to dismiss. I get an EXC_BAD_ACCESS on the removeFromSuperview line when it fails in the first method.
-(void)closeView:(UIViewController **)viewController
{
[UIView transitionWithView:self.view
duration:UINavigationControllerHideShowBarDuration
options:UIViewAnimationOptionCurveLinear
animations:^
{
[[*viewController view] setAlpha:0.0];
}
completion:^(BOOL finished)
{
[[*viewController view] removeFromSuperview];
[*viewController release], *viewController = nil;
}];
}
-(void)closeButtonClicked
{
[delegate closeView:&self];
}
//
// This method works without fail:
//
-(void)closeView
{
[UIView transitionWithView:self.view
duration:UINavigationControllerHideShowBarDuration
options:UIViewAnimationOptionCurveLinear
animations:^
{
// In this context viewController is defined in the class interface
[[viewController view] setAlpha:0.0];
}
completion:^(BOOL finished)
{
[[viewController view] removeFromSuperview];
[viewController release], viewController = nil;
}];
}
-(void)closeButtonClicked
{
[delegate closeView];
}
First of all, it is not according to the style guides, and not a good idea in general, to do a release of the viewController within a method like this. It will get you into trouble quickly. If the caller of this method is responsible for the viewController (it has done the retain), then it should release it as well. This is likely the cause of the first method not working from within the viewcontroller itself.
In the second method you do not pass in the viewController as parameter, which means it needs to be defined in the context.
If you don't release the viewController in this method, then you don't need to set its variable to nil either, and you can simply pass it as normal parameter:
-(void)closeView:(UIViewController *)viewController
{
[UIView transitionWithView:self.view
duration:UINavigationControllerHideShowBarDuration
options:UIViewAnimationOptionTransitionCrossDissolve
animations:^
{
[[viewController view] removeFromSuperview];
}
completion:nil];
}
you would then do this at the call-site:
[self closeView:childViewController];
[childViewController release]; childViewController = nil;
It safe to release the child in this way before the animation is done, because the animations block implicitly retains all objects referenced from the block, including the viewController parameter. Therefore, the child's dealloc is not called until the animations block releases it.
This does not work in your first code example, because you pass a pointer to a variable. That is, the animations block does not know it needs to retain the child.
BTW, I am not sure why you want to set the alpha, in the example above I show that you can also remove the view already in the animations block. See more about that in the UIView Class Reference.
**viewcontroller and &self is not the way to go. In Objective-C, you do [self.view removeFromSuperview] in the subview itself, in the parent viewcontroller you do release or with ARC just replace the subview with another view.
ViewController *vcObj = [[ViewController alloc]init];
[UIView transitionFromView:self.view toView:vcObj.view duration:2 options:UIViewAnimationOptionTransitionCurlUp completion:^(BOOL finished) {}];
[self release];
I am not releasing vcObj, if i release this app will crash and if i dont release this i get memory leak.
What is the standard way to do views transition or swaps between views?
i am new to this memory thing plz help me .. i studied books and tutorials but this situation i am unable to solve.
Release objects, as views or view controllers, in completion function. Animation is executed in another thread. If u release in Main Thread then objects can not exist when onvoked in the other.