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.
Related
I have a scene which is built as on the attached screen
Scene
Depending on the selection of a button from the top bar, I'm loading a specific view controller inside a container view. When selection is changed or the user is tapping the 'Back' button at controller then should be removed an intrinsic content of the container.
Code for adding into container view:
if (self.containerVC != nil) {
[self removeController];
}
UIStoryboard* storyboard = [UIStoryboard storyboardWithName:#"Main" bundle:nil];
FirstViewController* firstVC = [storyboard instantiateViewControllerWithIdentifier:#"First"];
[self addChildViewController:firstVC];
[self.containerVieww addSubview:firstVC.view];
firstVC.view.frame = CGRectMake(0, 0, self.containerVieww.bounds.size.width, self.containerVieww.bounds.size.height);
[firstVC.view layoutSubviews];
[firstVC didMoveToParentViewController:self];
firstVC.modalPresentationStyle = UIModalPresentationCurrentContext;
[UIView animateWithDuration:0.5 animations:^{
self.logViewHeight.constant = self.contentView.frame.size.height * 0.9;
[firstVC.view layoutSubviews];
}];
firstVC.delegate = self;
self.containerVC = firstVC;
Code for removing from container view:
[self.containerVC dismissViewControllerAnimated:YES completion:nil];
[UIView animateWithDuration:0.5 animations:^{
self.logViewHeight.constant = 0;
[self.view layoutIfNeeded];
}];
[self willMoveToParentViewController:nil];
[self removeFromParentViewController];
[self.containerVC.view removeFromSuperview]; // Caused layout destoying !!!
self.containerVC = nil;
After calling 'removeFromSuperview' is disappeared a 'Blue View' in my layout.
How to remove from the container view without destroying a complex layout?
I am using transitionWithView to have an animation when switching root view controller, but the animation options pvorided in UIViewAnimationOptions parameter does not have the animation like the presentViewController animation.
Is there a simple way to do it? Or how do I write an animation exactly the same as presentViewController animation?
Here's the animation I am using:
if (animated) {
[UIView transitionWithView:self.window duration:0.3
options:UIViewAnimationOptionTransitionFlipFromLeft
animations:^{
weakSelf.window.rootViewController = weakSelf.loginNavigationController;
}
completion:^(BOOL finished) {
}];
}
Here's how I do something similar in one of my apps.
I don't try to do this while changing root view controllers. Instead, I have one root view controller that is always there. I make this a custom parent view controller. Its child view controller looks like the root view controller because it occupies the whole window.
Now, when I replace the child view controller, I call transitionFromViewController:toViewController:duration:options:animations:completion:. This has the advantage that I can write my own animation.
As you can see, it is then trivial to have the animation be a slide up from the bottom; you are in complete control of how the view animates into place:
I follow matt's suggestion, made below code change:
first in didFinishLaunchingWithOptions:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
...
// the container view controller
self.window.rootViewController = [[UIViewController alloc] init];
if (!SharedInstance.accessToken) {
self.loginNavigationController = [self getNewLoginNaivigationController];
[self.window.rootViewController addChildViewController:self.loginNavigationController];
[self.window.rootViewController.view addSubview:self.loginNavigationController.view];
[self.loginNavigationController didMoveToParentViewController:self.window.rootViewController];
} else {
self.rootHomeViewController = [self getNewRootHomeController];
self.AppNavigationController = [self getNewAppNavigationControllerFromRootHomeViewController:self.rootHomeViewController];
[self.window.rootViewController addChildViewController:self.rootHomeViewController];
self.AppNavigationController.delegate = nil;
[self.window.rootViewController.view addSubview:self.rootHomeViewController.view];
[self.rootHomeViewController didMoveToParentViewController:self.window.rootViewController];
}
}
Then when I want to change it:
-(void)displayHomeViewControllerWithAnimation:(BOOL)animated {
if (!self.rootHomeViewController) {
self.rootHomeViewController = [self getNewRootHomeController];
self.AppNavigationController = [self getNewAppNavigationControllerFromRootHomeViewController:self.rootHomeViewController];
self.AppNavigationController.delegate = nil;
[self.window.rootViewController addChildViewController:self.rootHomeViewController];
}
// animation
__weak typeof(self) weakSelf = self;
[self.loginNavigationController willMoveToParentViewController:nil];
if (animated) {
[self.window.rootViewController transitionFromViewController:self.loginNavigationController
toViewController:self.rootHomeViewController duration:0.3
options:UIViewAnimationOptionCurveLinear animations:^{
// do animation here
} completion:^(BOOL finished) {
[weakSelf.rootHomeViewController didMoveToParentViewController:weakSelf.window.rootViewController];
[weakSelf.loginNavigationController removeFromParentViewController];
weakSelf.loginNavigationController = nil;
}];
} else {
[weakSelf.rootHomeViewController didMoveToParentViewController:weakSelf.window.rootViewController];
[weakSelf.loginNavigationController removeFromParentViewController];
weakSelf.loginNavigationController = nil;
}
}
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];
}];
}
Alright so I am trying to create a slide out side view for navigation in my app, similar to Facebook and many others. I have that part working but the navigation controller doesn't respond to any touch events. I have a feeling this is because I am having my subclassed Navigation Controller add the child view controller and subview, instead of having the present view display the child. Here is the code I run on my "FeaturedViewController" that calls to "MainNavigationController" to add "MenuViewController" as a child of "MainNavigationController"
//ignoring compiler error as we know the parentViewController will be of type MainNavigationController at runtime.
MainNavigationController *main = self.parentViewController;
if (self.categories != nil)
main.categories = self.categories;
if (main.showingLeftPanel)
[main movePanelToOriginalPosition];
else
[main movePanelRight];
From there:
- (void)movePanelRight // to show left panel
{
UIView *childView = [self getLeftView];
//_leftPanelViewController.categories = self.categories;
[self.view sendSubviewToBack:childView];
[UIView animateWithDuration:SLIDE_TIMING delay:0 options:UIViewAnimationOptionBeginFromCurrentState
animations:^{
_centerViewController.view.frame = CGRectMake(self.view.frame.size.width - PANEL_WIDTH, 0, self.view.frame.size.width, self.view.frame.size.height);
}
completion:^(BOOL finished) {
if (finished) {
//_centerViewController.leftButton.tag = 0;
}
}];
}
- (UIView *)getLeftView
{
// init view if it doesn't already exist
if (_leftPanelViewController == nil)
{
// this is where you define the view for the left panel
_leftPanelViewController = [self.storyboard instantiateViewControllerWithIdentifier:#"MenuViewController"];
[self addChildViewController:_leftPanelViewController];
[self.view addSubview:_leftPanelViewController.view];
[_leftPanelViewController didMoveToParentViewController:self];
_leftPanelViewController.view.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height);
}
_leftPanelViewController.categories = self.categories;
self.showingLeftPanel = YES;
// set up view shadows
[self showCenterViewWithShadow:YES withOffset:-2];
UIView *view = self.leftPanelViewController.view;
return view;
}
I've made a test app to familiarize myself with making a custom container view controller. If I rotate the device when the app first starts or after switching to a different view controller, the new view resizes to take up the whole screen, as I intended. However, if I rotate after the app starts, and then switch to a new view controller, the view keeps its portrait size instead of getting shorter and wider (actually it's slightly different -- it goes from 320,460 to 300,480). The master view controller is alloc init'd in the app delegate (no xib) and set as the window's root view controller. Here is the code I have in my MasterViewController (the custom container controller):
- (void)viewDidLoad {
[super viewDidLoad];
WelcomeController *welcome = [[WelcomeController alloc] initWithNibName:#"ViewController" bundle:nil];
self.currentController = welcome;
[self addChildViewController:welcome];
[self.view addSubview:welcome.view];
UISwipeGestureRecognizer *swipeLeft = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(swipeLeft:)];
swipeLeft.direction = UISwipeGestureRecognizerDirectionLeft;
[self.view addGestureRecognizer:swipeLeft];
}
- (void)swipeLeft:(UISwipeGestureRecognizer *)gesture {
if (gesture.state == UIGestureRecognizerStateRecognized) {
UIActionSheet *sheet =[[UIActionSheet alloc] initWithTitle:#"Select A Destination" delegate:self cancelButtonTitle:#"Cancel" destructiveButtonTitle:nil otherButtonTitles:#"welcome",#"Play",#"Scores", nil];
[sheet showInView:self.view];
}
}
- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex {
switch (buttonIndex) {
case 0:{
if ([self.currentController class] != [WelcomeController class] ) {
WelcomeController *welcome = [[WelcomeController alloc] initWithNibName:#"ViewController" bundle:nil];
[self addChildViewController:welcome];
[self moveToNewController:welcome];
}
break;
}
case 1:{
if ([self.currentController class] != [PlayViewController class] ) {
PlayViewController *player = [[PlayViewController alloc] initWithNibName:#"PlayViewController" bundle:nil];
[self addChildViewController:player];
[self moveToNewController:player];
}
break;
}
case 2:{
if ([self.currentController class] != [HighScores class] ) {
HighScores *scorer = [[HighScores alloc] initWithNibName:#"HighScores" bundle:nil];
[self addChildViewController:scorer];
[self moveToNewController:scorer];
}
break;
}
case 3:
NSLog(#"Cancelled");
break;
default:
break;
}
}
-(void)moveToNewController:(id) newController {
[self.currentController willMoveToParentViewController:nil];
[self transitionFromViewController:self.currentController toViewController:newController duration:.6 options:UIViewAnimationOptionTransitionCrossDissolve animations:^{}
completion:^(BOOL finished) {
[self.currentController removeFromParentViewController];
[newController didMoveToParentViewController:self];
self.currentController = newController;
}];
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
return YES;//(interfaceOrientation == (UIInterfaceOrientationPortrait | UIInterfaceOrientationLandscapeLeft));
}
Any ideas why this is happening (I don't know if this means that the master view controller's view isn't resizing, but when I get this non-resizing behavior the gesture recognizer only responds in the narrow view, not over the whole screen)?
You are not sending rotation messages to your child view controllers. At least not in the code you posted. After switching Child Controller you are even removing previous child from ChildViewControllers array with [self.currentController removeFromParentViewController] so even if you implement - (BOOL)automaticallyForwardAppearanceAndRotationMethodsToChildViewControllers you have always only one ChildViewController in your ParentViewController.
I have got this working, so i will paste you how am i doing this. First i create all my ViewControllers, add them as child view controllers to ParentViewController. Then call didMoveToParentViewController: method.
//Controller1
Controller1 *c1 = [[Controller1 alloc] init];
c1.view.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
[self addChildViewController:c1];
[c1 didMoveToParentViewController:self];
//Controller2
Controller2 *c2 = [storyboard instantiateViewControllerWithIdentifier:#"c2"];
index.view.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
[self addChildViewController:c2];
[c2 didMoveToParentViewController:self];
c2.view.frame = m_contentView.frame;
[self.view addSubview:c2.view]; //It is in initial screen so set it right away
m_selectedViewController = c2;
//Controller3
Controller3 *c3 = [storyboard instantiateViewControllerWithIdentifier:#"c3"];
compare.view.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
[self addChildViewController:c3];
[c3 didMoveToParentViewController:self];
m_controllers = [NSArray arrayWithObjects:c1, c2, c3, nil]; //Hmm now i think this is not needed, I can access viewController directly from self.childViewControllers array
Then I implemented
- (BOOL)automaticallyForwardAppearanceAndRotationMethodsToChildViewControllers
{
return YES;
}
Switching to child view controller is done with this code
if (value < m_controllers.count)
{
UIViewController *contentViewController = [m_controllers objectAtIndex:value];
contentViewController.view.frame = m_contentView.frame;
[self transitionFromViewController:m_selectedViewController toViewController:contentViewController duration:0 options:UIViewAnimationOptionTransitionNone animations:nil completion:^(BOOL finished) {
m_selectedViewController = contentViewController;
}
];
}
This should be enough. But i have got some issues with this so i send rotation messages to unactive Childs myself.
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
for (UIViewController *vc in m_controllers)
{
if(vc != m_selectedViewController)
[vc willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration];
}
}
- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
for (UIViewController *vc in m_controllers)
{
if(vc != m_selectedViewController)
[vc willAnimateRotationToInterfaceOrientation:toInterfaceOrientation duration:duration];
}
}
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
for (UIViewController *vc in m_controllers)
{
if(vc != m_selectedViewController)
[vc didRotateFromInterfaceOrientation:fromInterfaceOrientation];
}
}
manually added
self.view.autoresizesSubviews = YES;
inside
- (void)viewDidLoad
and it solved the problem,
for some reason the value inside storyboard was not being used i guess