view controllers: presentation, dismissal

2019-08-10 08:22发布

Just for the purpose of learning some particular aspects of xCode, I am creating a simple app that has 2 functional view controllers. Each contains a button that can be pressed to switch to the other. I am not using segues. I am using pointers retrieved from the app delegate.

visual illustration (click for higher resolution): visual

When the app loads, the root view controller presents view 1. When you click "switch to view 2," the following code causes view 2 to appear:

- (IBAction)buttonPressed:(id)sender
{
    AppDelegate *appDelegate = (AppDelegate*)[[UIApplication sharedApplication] delegate];
    [self presentViewController:appDelegate.view2 animated:YES completion:nil];
}

So far, so good.

But when you click "switch to view 1" on the second view, this same code (replacing "view2" with "view1") gives the following error:

Application tried to present modally an active controller.

So to summarize (where --> = presents), we have root --> view1 --> view2 -x-> view1

I don't care about retaining the history of who presents whom. I simply want the buttons to bring to the top (make visible) a previously displayed view controller, maintaining the state of its views.

It would be nice to know the following:

  1. Is there a workaround that would enable me to achieve the intended behavior using presentViewController? E.g., root --> view2 --> view1

  2. What other method(s) would be more practical for achieving the desired behavior? It/they must use the app delegate because in my real application that will be unavoidable.

  3. Am I breaking the rules by trying to put a view controller on top without integrating into some larger architecture? E.g, is this sort of behavior supposed to be handled by navigation conrollers and pushing/popping? If so, can you explain why xCode doesn't want me to do this? Why can't I just display whatever view controller I want, without it necessarily having any relationship to other view controllers? (Maybe because that could lead to abuse of the app delegate?)

  4. What does it really mean to "present" a view controller? What functional limitations or capabilities does it entail beyond creating pointers between presenting and presenter? What is the importance of leaving the presenting view controller "active"?

  5. If instead make the button on view1 send the presentViewController message to the root view (which I hoped would just change the presentation chain from root --> view1 to root --> view2, leaving view1 still existing in memory but not part of this chain), I get a different error: "Attempt to present on whose view is not in the window hierarchy!" What does this mean? I can't find an explanation of window hierarchy.

Okay, I know I'm asking a lot here, but any amount of enlightenment will be greatly appreciated!!

2条回答
闹够了就滚
2楼-- · 2019-08-10 08:56

Let me try to tackle your points one by one.

1) No, you shouldn't do this all with presentViewController.

2) If you want to do root --> view1 --> view2 --> view1, then you don't do that all with presentViewController. To go from view1 back to view2 you should use dismissViewControllerAnimated:completion.

3) The view controllers do have a relationship when you use presentViewController:animated:. The presenting controller has a pointer to the one it presents, and the presented one has a pointer to the one that presented it. So, you're getting these relationships whether you want it or not. There is a way to display whatever controller you want with no relationship between them -- just reset the window's root view controller. The old view controller will be deallocated (if you don't keep a strong pointer to it), and the new one becomes the window's root view controller.

4) Presenting a view controller makes that controller a modal view controller -- it takes over the whole screen and is intended to be used as an interruption in the flow of the app. You really shouldn't use them extensively to go from one controller to another (and especially not for going "backwards" to previous controllers). Because of the way it's supposed to be used, you normally want to go back to the controller that presented it, so that's why it's kept "active" (in the sense that it's not deallocated).

5) You get that error because root's view is not on screen, view1's is. You need to present a view controller from the controller on screen.

查看更多
我只想做你的唯一
3楼-- · 2019-08-10 08:57

The correct way to do this is to get the underlying rootVC to do the presenting and dismissing (as you attempt - without the dismissing part - in point 5). You can achieve this by sending a message + completion block back to the rootVC from each of view1 and view2 when you want to present the other.

When you are in view1:

- (IBAction)buttonPressed:(id)sender 
    AppDelegate *appDelegate = (AppDelegate*)[[UIApplication sharedApplication] delegate];

    UIViewController* presentingVC = self.presentingViewController;
    [presentingVC dismissViewControllerAnimated:YES completion:^{
          [presentingVC presentViewController:appDelegate.view2 
                                     animated:YES 
                                   completion:nil];
    }];
}

and similarly for view2. Take care that you need this line:

        UIViewController* presentingVC = self.presentingViewController;

as you can't refer to 'self.presentingViewController' inside the completion block as it's controller has been dismissed at this point.

I think that answers points 1 and 2.

To answer point 3 "Why can't I just display whatever view controller I want, without it necessarily having any relationship to other view controllers?" - well you can (via the rootViewController property of the window), but then you are going to have to implement navigation and manage your viewController pointers, which means you will end up creating a controller of some sort. Apple is helping you here by providing you with a few which cover most needs.

As regards your point 4 - the presenting of a viewController is controlled by the presenting VC, which is why you want to keep that one 'active'. When you send this message: [self dismissViewControllerAnimated:completion:], self just reroutes the messge to it's presentingViewController. If you get rid of your presentingViewController your dismiss method will break.

Point 5 is answered above. You need to dismiss the topmost view first before asking an underlying view to present. Note that view1 is "still in memory" but only because you have retained a pointer to it in your app delegate.

update

As you are trying to get this to work with an initial launch-straight-to-view1, you could make a BOOL launched property and check/set it from your rootViewController's viewDidAppear:

- (void) viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    if (!self.launched) {
        self.launched = TRUE;
        AppDelegate *appDelegate = (AppDelegate*)[[UIApplication sharedApplication] delegate];
        [self presentViewController:appDelegate.view1
                           animated:YES
                         completion:nil];
    }
}
查看更多
登录 后发表回答