I have a UIPageViewController subclass that shows images. This view controller is inside a larger view controller that has other content. I want to be able to tap on an image in the page view controller and have that page view controller removed from where it is and presented full screen, where additional controls such as zooming and panning around the image would be available. Then, I also need a way to be able to dismiss it from being presented full screen and to re-insert it in the original parent view controller.
- (void)handleTapGesture {
UIViewController *parentViewController = self.parentViewController;
[self didMoveToParentViewController:nil];
[self.view removeFromSuperview];
[self removeFromParentViewController];
self.modalPresentationStyle = UIModalPresentationFullScreen;
[parentViewController presentViewController:self animated:YES completion:nil];
}
But when I do this, I can see the dimming view and everything that is set up automatically when presenting the view controller, but the view controller itself is not visible.
I viewed it in the view debugger, but it looks like the frame of the page view controller is zero sized. Here is some output from the debugger:
Unbalanced calls to begin/end appearance transitions for <MyPageViewController: 0x10ca8f000>.
Printing description of $21:
<_UIPageViewControllerContentView: 0x117b04c40; frame = (0 0; 0 0); clipsToBounds = YES; opaque = NO; autoresize = W+H; layer = <CALayer: 0x283491be0>>
I am not sure why that is though and I do not know how to debug this since I am not specifying any layout explicitly. From what I understand, when I present this view controller, I should not have to specify any constraints or sizes as that is to be handled by the view controller transition. All I am doing is trying to make a view controller, that was a child view controller, be presented modally full screen.
The view containment calls are incorrect. (See below.)
But the “Unbalanced calls” error message suggests that there might be some other, deeper problem elsewhere in your code base. The incorrect view controller containment calls are insufficient to manifest this error.
One generally gets this error when attempting to initiate a transition while another is underway (e.g., trying to present/dismiss view controllers inside the viewDidLoad, viewWillAppear or viewWillDisappear methods).
But the supplied code snippet is insufficient to manifest the problem you describe. We need MCVE. I would suggest you create a blank project and figure out what you need to add to it in order to manifest your error.
That having been said, the correct view controller containment calls to remove a child are willMoveToParentViewController, followed by removeFromSuperview, followed by removeFromParentViewController, e.g.:
[self willMoveToParentViewController:nil];
[self.view removeFromSuperview];
[self removeFromParentViewController];
Note, I did not call didMoveToParentViewController, because, as the documentation says:
The removeFromParentViewController method automatically calls the didMoveToParentViewController: method of the child view controller after it removes the child.
Obviously, when adding a child, the converse is true, that you do not call willMoveToParentViewController, but you do call didMoveToParentViewController:
[self addChildViewController:child];
[self.view addSubview:child.view];
child.view.frame = ...;
[self didMoveToParentViewController:self];
Again, the documentation advises us:
When your custom container calls the addChildViewController: method, it automatically calls the willMoveToParentViewController: method of the view controller to be added as a child before adding it.
Related
I have a UINavigationController, containing a UIViewController that is parent to two UITableViewController controllers.
When the user taps on a segmented control in the UIToolbar of the navigation controller, the current child table controller is swapped out with the new one. This includes removing the old controller from the parent hierarchy and removing its view as a subview of the parent view controller.
The first view controller that is displayed when the navigation view controller first presents it has its contentInset correctly configured by automaticallyAdjustsScrollViewInsets, however, when I pull that one out and insert the view from the second table view controller, that does not.
Furthermore, if I rotate the device (Which shrinks the UINavigationBar) and then swap back to the first view controller, its contentInset is now incorrect and it doesn't scroll properly. The second controller, however, does have its contentInset property properly set as a result of the device rotation.
Is there a way to manually force a UIViewController to redo its automaticallyAdjustsScrollViewInsets operation when I need it?
It's not an absolutely amazing one, but I found a solution that works.
Inserting a new child view controller isn't enough to trigger UINavigationController to automatically work out the appropriate contentInset values for any scroll views in the new child. BUT! You can force it to perform that calculation by doing something that would have required it anyway. For example, hiding and showing the navigation bar or toolbar.
- (void)insertViewController:(UIViewController *)viewController
{
// Add the view to our view
viewController.view.frame = self.view.bounds;
[self.view addSubview:viewController.view];
// Add the new controller as a child
[self addChildViewController:viewController];
[viewController didMoveToParentViewController:self];
// Show and hide the toolbar to force the content inset calculation
self.navigationController.toolbarHidden = YES;
self.navigationController.toolbarHidden = NO;
}
I've tested it, and there appear to be no visual glitches by rapidly hiding either the navigation bar or toolbar, so this solution seems to be acceptable.
I'm adding 5 viewcontrollers to a scrollview with a page control so I can swipe from one viewcontroller to another.
At initialization I'm loading 3 viewcontrollers (left, middle, right)
My problem is that my three viewcontrollers are firing a viewDidAppear but only the center viewController is visible...
Is there a way to avoid the view controllers that aren't visible to call viewdidappear?
I'm adding my viewcontrollers to my scrollview like so :
if (controller != nil){
[self addChildViewController:controller];
[controller didMoveToParentViewController:self];
}
[scrollView addSubview:controller.view];
No, the viewDidAppear method is not related to the visibility of the view, if you get the documentation you will see
Notifies the view controller that its view was added to a view hierarchy.
So this method will be called when the view is added to the hierarchy. So the view was loaded, and added to the hierarchy, even in a non visible space of your mainView, it will call viewDidAppear.
To achieve what you want, you should implement the delegate of the scrollView, check the offset, and see in which page you are, then you can call a method on your viewController to do the job you want.
In my app, I have a nav bar and two toolbars visible in the main View Controller. I then have a container view in that main VC that fills the screen, behind the UI elements. I embedded a UIPageViewController in the container. I then set up a UICollectionViewController scene that is reused when swiping between the pages. This is done in code - see below. Basically, it presents the UICollectionViewController on screen.
It's working well, except when it appears on screen it covers up the UI of the main VC. I could change the height to reveal the UI, but I do want it to take up the entire screen. I just want it to lie behind the toolbars. That way when the user scrolls the Collection VC the content will be rendered underneath those translucent toolbars.
I understand why it's behaving the way it is, but I don't know how to fix it. I originally figured since it's all embedded in a Container it would respect the z-order of that container which appears fine in the storyboard, but I suppose if I'm just throwing the VC on screen in code I should expect such behavior. I'm just not sure how to get the desired behavior. Thanks for the assistance!
//the following code is in the main view controller
//set first page view controller
ContentCollectionViewController *startingViewController = [self viewControllerAtIndex:0]; //calls method below
[self.pageViewController setViewControllers:#[startingViewController]
direction:UIPageViewControllerNavigationDirectionForward
animated:NO
completion:nil];
//move page view controller down to reveal the underlying UI and show my issue
self.pageViewController.view.frame = CGRectMake(0, 80, self.view.frame.size.width, self.view.frame.size.height);
//display created page VC
[self addChildViewController:self.pageViewController];
[self.pageViewController didMoveToParentViewController:self];
[self.view addSubview:self.pageViewController.view];
//in the viewControllerAtIndex method:
ContentCollectionViewController *contentCollectionViewController = [self.storyboard instantiateViewControllerWithIdentifier:#"ContentCollectionViewController"];
//set properties here...
return contentCollectionViewController;
I was finally able to solve it by changing
[self.view addSubview:self.pageViewController.view];
to
[self.view insertSubview:self.pageViewController.view atIndex:1];
I am trying to present a UIViewController with a UIView on it.
The following is the code I am trying in my viewDidLoad method.
//create the view controller
UIViewController *controller = [[UIViewController alloc] init];
UIView *view = [[UIView alloc] init];
view.backgroundColor = [UIColor whiteColor];
controller.view = view;
//show the view
[self presentViewController:controller animated:YES completion:nil];
When I run the app, it is giving me the following error.
Warning: Attempt to present <UIViewController: 0x751fcd0> on <ViewController: 0x751d7a0> whose view is not in the window hierarchy!
What does this mean and where am I going wrong? Shouldn't it display a white view or am I understanding wrong?
Thanks.
The solution is to move my code to the viewDidAppear method.
I'm assuming that the view controller's view is not in the window hierarchy at the point that it has been loaded (when the viewDidLoad message is sent), but it is in the window hierarchy after it has been presented (when the viewDidAppear: message is sent).
If you call presentViewController:animated:completion from 'viewDidLoad:' it won't work. And that is why:
The area of the screen used to define the presentation area is determined by the presentation context. By default, the presentation context is provided by the root view controller, whose frame is used to define the frame of the presentation context. However, the presenting view controller, or any other ancestor in the view controller hierarchy, can choose to provide the presentation context instead. In that case, when another view controller provides the presentation context, its frame is used instead to determine the frame of the presented view. This flexibility allows you to limit the modal presentation to a smaller portion of the screen, leaving other content visible.
View Controller Programming Guide for iOS: Presenting View Controllers from Other View Controllers
In viewDidLoad frame of presenting view controller simply not set yet. That is why you should present next controller only when presenting controller is on screen.
I'm doing some "interesting" view transitions, and I'm finding myself working around the functionality of "presentModalViewController" in a way that doesn't feel right.
I'd prefer to take total control over the presentation of the modal view controller's view and skip "presentModalViewController" altogether.
However, I'm not sure about the ramifications of doing this.
Currently, I've got code that looks works something like like this (this is just a pseudo-code example, and I can't use the built in transitions, they won't do what I need):
// Create the child view controller:
ModalViewController * child = [[ModalViewController alloc] init];
// Present it:
[parentViewController presentModalViewController:child animated:NO];
// This rect is what the child view's ultimate "destination" should be,
// and, what the parent view's old frame was:
CGRect frame = child.view.frame;
// Put the parent view controller's view back in the window:
[child.view.window insertSubview:parentViewController.view belowSubview:child.view];
// Show it if it's hidden:
[parentViewController.view setHidden:NO];
// Put the parent back where it was:
[parentViewController.view setFrame:frame];
// Put the child at the "top" of the screen (the bottom edge
// of the child's view is at the top of the screen):
[child.view setFrame:CGRectMake(frame.origin.x,
frame.origin.y - frame.size.height,
frame.size.width,
frame.size.height)];
// Animate a transition which slide the parent and child views
// down together:
[UIView animateWithDuration:0.7 animations:^(void) {
child.view.frame = frame;
parentViewController.view.frame = CGRectMake(frame.origin.x,
frame.origin.y + frame.size.height,
frame.size.width,
frame.size.height);
} completion:^(BOOL finished) {
// We're done, remove the parent view from the window
// like it's supposed to be:
[parentViewController.view removeFromSuperview];
}];
[child release];
If you don't want to have UIKit set modalViewController and control the presentation and dismissal of the child view controller, then don't. You can skip the presentModalViewController:animated: call and manually add or remove subviews, or if you want to switch to an entirely new view controller then disconnect the old one's view from the heirarchy and connect the new one, etc. Other ways of presenting include UINavigationController or a UITabBarController, and they don't use the modalViewController methods.
To be more specific, you should set the rootViewController property of your application's UIWindow to the new view controller.
Docs say:
The root view controller provides the content view of the window. Assigning a view controller to this property (either programmatically or using Interface Builder) installs the view controller’s view as the content view of the window. If the window has an existing view hierarchy, the old views are removed before the new ones are installed.
Note that the docs mention an automatic process of installing the view as the content view of the heirarchy. What I'm saying is you can use the provided automatic methods - UIWindow for root views, modalViewController and other systems for non-root views - or you can do it manually, but it's accomplishing the same thing. Particularly since the rootViewController property has only existed since iOS 4, and applications prior to this used auto-generated default code of [window addSubview:rootView] at launch.
If UIKit has some extra magic occurring in [UIWindow setRootViewController:] I'm totally prepared to be corrected on this though.