Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do Modal and Child View Controllers interact?

I have a custom container view controller: ContainerVC. Its job is to present one of two content view controllers: ContentPortraitVC or ContentLandscapeVC, depending on the current orientation (though it doesn't matter why the container chooses its view, I presume). ContentPortraitVC, at some point pops up ContentModalDetailVC.

So there are two different methods of displaying new content at work here:

  • the parent-and-child relationship (instigated via addChildViewController and removed via removeFromParentViewController),

  • the presenting-and-presented relationship (instigated via presentViewController and removed via dismissViewController).

If the ContainerVC adds the ContentPortraitVC, which then presents the ContentModalDetailVC, and then the ContainerVC decides to switch to the ContentLandscapeVC, the ContentModalDetailVC stays visible (why is it not removed when its parent is removed?)

But then, when the ContentPortraitVC is asked to remove the ContentModalDetailVC, nothing happens. The modal display stays put. What is going on?

like image 602
Ian Avatar asked Aug 04 '13 20:08

Ian


People also ask

What is a child view controller?

Adding and removing child view controllers ensures that the parent view controller knows of its children. This will help when doing things like calling a modal from a child view that has been added to the parent. This behavior can be buggy if the parent doesn't know that it's connected to the child.

What is child view controller in Swift?

Child view controllers are especially useful for UI functionality that we wish to reuse across a project. For example, we might want to display a loading view as we're loading the content for each screen — and that can easily be implemented using a child view controller, that can then simply be added when needed.


2 Answers

  1. When you use addChildViewController to add the ContentPortraitVC:

    a. The ContentPortraitVC gets its parentViewController property set.

    b. You then (as per the Apple documentation) have to manually display the ContentPortraitVC's view. If you follow the documentation you do this by adding it as a child of the ControllerVC's top level view.

  2. The ContentPortraitVC then calls presentViewController to display ContentModalDetailVC.

    a. This sets its presentingViewController property (in the debugger this is shown as the _parentModalViewController ivar -- note the ivar is different from the property), and sets the presentedModalViewController property of the ContentPortraitVC (who's ivar is _childModalViewcontroller).

    b. Views wise, on iPhone, the ContentModalDetailVC's view will completely replace the views from ContentPortraitVC and ContainerVC, so only the modal view controller's view will be visible. (on iPad, it layers the new UI over the top, but as a sibling of the ControllerVC's view, which in turn is the parent of ContentPortraitVC's view).

  3. So now, you transition from ContentPortraitVC to ContentLandscapeVC.

    a. IOS does a bit of magic. It knows that the thing you are removing (ContentPortraitVC) has a presentedViewController currently active, so it changes its parent. It sets the value to nil on ContentPortraitVC, takes the child (the ContentModalDetailVC) and sets its parent to the new view (ContentLandscapeVC). So now the view controller that presented the modal view is no longer its presenting view controller. It is as if ContentLandscapeVC presented it in the first place!

    b. In terms of views, you follow the Apple docs to change over the view from ContentPortraitVC to ContentLandscapeVC. But you are simply changing the subviews of ControllerVC's view. On iPhone, the modal view controller is still the only thing being displayed, so making the change doesn't do anything on screen. On iPad, it does (though you probably won't see it, as the modal view is usually full screen).

  4. Now you come to dismiss the modal view. Presumably you do this in ContentPortraitVC, but it no longer has any reference to the thing it presented. So calling [self dismissViewController... does nothing, because ContentPortraitVC is no longer presenting anything, responsibility for that has been passed on to ContentLandscapeVC.

So that's what happens and why. Here's what to do about it.

  1. You can rewire the delegate manually when you change from ContentPortraitVC to ContentLandscapeVC, so the latter is the one that tries to dismiss the modal controller.

  2. You can have the modal controller dismiss itself with [self dismissModalControllerAnimated:YES completion:nil]. I'm going to ask and answer another question on why that works (how does IOS know which to dismiss?), if that seems strange.

  3. You can have the ControllerVC be the one that pops up the modal view and be responsible for removing it.

like image 187
4 revs Avatar answered Sep 22 '22 02:09

4 revs


If you inspect presentingViewController on ContentModalDetailVC, you will see that it is actually presented by ContainerVC and not ContentPortraitVC.

To fix this, you just need to set definesPresentationContext (or use the "Defines Context" checkbox in Interface Builder) on ContentPortraitVC.

This will tell ContentPortraitVC to handle the modal presentation instead of passing up the responder chain to the next view controller that defines presentation context (your root view controller by default).

You will probably want ContentLandscapeVC to define context as well to avoid the same issue.

With both child controllers defining their own presentation context, when ContainerVC decides to swap children, any modal modal will be removed from the new hierarchy along with the child that presented it. No need to do hacky things to try to dismiss before swapping :)

Edit: I should add that the view controller being presented must have its modalPresentationStyle Set to either currentContext or overCurrentContext,

like image 33
RyanM Avatar answered Sep 18 '22 02:09

RyanM