Warning: Attempt to present ViewController on ViewController whose view is not in the window hierarchy

后端 未结 5 926
天命终不由人
天命终不由人 2020-12-29 12:33

I have already looked through related questions but nothing has solved my problem.

I am attempting to use dismissViewControllerAnimated:animated:completion

相关标签:
5条回答
  • 2020-12-29 13:10

    About this,

    - (IBAction)emailMe:(id)sender {
        [self dismissViewControllerAnimated:YES completion:^{
            [self sendMeMail];
        }];
    }
    

    After dismissing self viewController, you cannot present view controllers from self.

    Then what you can do ?

    1) Change the button press method,

    - (IBAction)emailMe:(id)sender {
        [self sendMeMail];
    }
    

    2) You can dismiss the self viewController, when the mailViewController is dismissed.

    - (void)mailComposeController:(MFMailComposeViewController*)controller
              didFinishWithResult:(MFMailComposeResult)result
                            error:(NSError*)error;
    {
        if (result == MFMailComposeResultSent) {
            NSLog(@"It's sent!");
        }
        [controller dismissViewControllerAnimated:NO completion:^ {
            [self dismissViewControllerAnimated:YES completion:nil];
        }];
    
    }
    
    0 讨论(0)
  • 2020-12-29 13:13

    It's a communications issue between view-controllers resulting out of an unclear parent-child view-controller relationship... Without using a protocol and delegation, this won't work properly.

    The rule of thumb is:

    • Parents know about their children, but children don't need to know about their parents.

    (Sounds heartless, but it makes sense, if you think about it).

    Translated to ViewController relationships: Presenting view controllers need to know about their child view controllers, but child view controllers must not know about their parent (presenting) view controllers: child view controllers use their delegates to send messages back to their (unknown) parents.

    You know that something is wrong if you have to add @Class declarations in your headers to fix chained #import compiler warnings. Cross-references are always a bad thing (btw, that's also the reason why delegates should always be (assign) and never (strong), as this would result in a cross-reference-loop and a group of Zombies)


    So, let's look at these relationships for your project:

    As you didn't say, I assume the calling controller is named MainController. So we'll have:

    • A MainController, the parent, owning and presenting the InfoController
    • An InfoController (revealed partially below MainController), owning and presenting a:
    • MailComposer, which cannot be presented because it would be displayed below the MainController.

    So you want to have this:

    • A MainController, the parent, owning and presenting the InfoController & MFMailController
    • An InfoController (revealed partially below MainController)
    • an "Email-Button" in the InfoController's view. On click it will inform the MainController (it's unknown delegate) that it should dismiss the InfoController (itself) and present the MailComposer
    • an MailComposer that will be owned (presented & dismissed) by the MainController and not by the InfoController

    1. InfoController: Defines a @protocol InfoControllerDelegate:

    The child controller defines a protocol and has a delegate of unspecified type which complies to its protocol (in other words: the delegate can be any object, but it must have this one method)

    @protocol InfoControllerDelegate
    - (void)returnAndSendMail;
    @end
    
    @interface InfoControllerDelegate : UIViewController // …
    
    @property (assign) id<InfoControllerDelegate> delegate
    
    // ...
    
    @end
    

    2. MainController owns and creates both InfoController and MFMailController

    ...and the MainController adopts both the InfoControllerDelegate and the MFMailComposeDelegate protocol, so it can dismiss the MFMailComposer again (Note, that doesn't and probably shouldn't need to be strong properties, just showing this here to make it clear)

    @interface MainController <InfoControllerDelegate, MFMailComposeViewControllerDelegate>
    
    @property (strong) InfoController *infoController;
    @property (strong) MFMailComposeViewController *mailComposer;
    

    3. MainController presents its InfoViewController and sets itself as the delegate

    // however you get the reference to InfoController, just assuming it's there
    infoController.delegate = self;
    [self presentViewController:infoController animated:YES completion:nil];
    

    The 'infoController.delegate = self' is the crucial step. This gives the infoController a possibility to send a message back to the MainController without knowing it ObjectType (Class). No #import required. All it knows, it that it's an object that has the method -returnAndSendMail; and that's all we need to know.

    Typically you would create your viewController with alloc/init and let it load its xib lazily. Or, if you're working with Storyboards and Segues, you probably want to intercept the segue (in MainController) in order to set the delegate programmatically:

    - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
        // hook in the segue to set the delegate of the target
        if([segue.identifier isEqualToString:@"infoControllerSegue"]) {
            InfoController *infoController = (InfoController*)segue.destinationViewController;
            infoController.delegate = self;
        }
    }
    

    4. In InfoController, the eMail button is pressed:

    When the eMail button is pressed, the delegate (MainController) is called. Note that it's not relevant that self.delegate is the MainController, it's just relevant that it has this method -returnAndSendMail

    - (IBAction)sendEmailButtonPressed:(id)sender {
        // this method dismisses ourself and sends an eMail
        [self.delegate returnAndSendMail];
    }
    

    ...and here (in MainController!), you'll dismiss the InfoController (clean up because it's the responsibility of the MainController) and present the MFMailController:

    - (void)returnAndSendMail {
        // dismiss the InfoController (close revealing page)
        [self dismissViewControllerAnimated:YES completion:^{
            // and present MFMailController from MainController
            self.mailComposer.delegate = self;
            [self presentViewController:self.mailComposer animated:YES completion:nil];
        }];
    }
    

    so, what you're doing with the MFMailController is practically the same as with the InfoController. Both have their unknown delegate, so they can message back and if they do, you can dismiss them and proceed with whatever you should to do.

    Notes

    • -dismissViewControllerAnimated:completion: should not be called from the child view controller. In the docs, it says: "The presenting view controller is responsible for dismissing the view controller it presented.". That's why we still need delegation. And it's useful, because the relationships and responsibilities of parents are important! Indeed. You can't create something and then just leave it be. Well, you can, but you shouldn't.
    • if you wouldn't use a revealing view controller animation, you could chain these Parent (adopting Child Protocol) - Child (defining protocol for parent and adopting protocol for grandchild) - Grandchild (defining protocol for ...
    • Again: a design where one MainController is owning and presenting all the child viewController is really a bad design. So the solution presented is about protocols and communication and not about putting everything in one MainController
    • I don't think that blocks as a coding technology free us from the need to define relationships and declare Protocols
    • Hope that helps
    0 讨论(0)
  • 2020-12-29 13:25

    If you're using storyboards, try checking the transition types you're using on your segues. You will have issues dismissing layers of viewcontrollers you transition to modally. This may be the source of your problems. Try switching them to push instead. enter image description hereAlthough you can write delegates to accomplish dismissing viewcontrollers it really shouldn't be needed. This is an overly complicated solution. Image if you have a viewcontroller that transitions to dozens of different storyboards, are you going to have dozens of delegates controlling dismissal? It seems suboptimal.

    0 讨论(0)
  • 2020-12-29 13:33

    When you present a viewController, the viewController you are presenting from needs to be in the view hierarchy. The two VC's hold pointers to each other in their properties presentingViewController and presentedViewController, so both controllers need to be in memory.

    By dismissing then running presenting code from the being-dismissed view controller, you are breaking this relationship.

    Instead, you should be doing the presenting of your mailController from the viewController that presented InfoController, after the infoController has been dismissed.

    The trad way to do this is via a delegate callback to the underlying viewController which then handles the two steps of dismissing and presenting. But now we use blocks..

    Move your sendMeMail method into the VC that presented infoController

    Then - in infoController - you can call it in the completion block…

    - (IBAction)emailMe:(id)sender {
        UIViewController* presentingVC = self.presentingViewController;
        [presentingVC dismissViewControllerAnimated:YES completion:^{
          if ([presentingVC respondsToSelector:@selector(sendMeMail)])
              [presentingVC performSelector:@selector(sendMeMail) 
                                 withObject:nil];
        }];
    }
        
    

    (you need to get a local pointer to self.presentingViewController because you cannot refer to that property after the controller has been dismissed)

    Alternatively keep all of the code in infoController by putting the sendMeMail code in the completion block:

    - (IBAction)emailMe:(id)sender {
        UIViewController* presentingVC = self.presentingViewController;
        [presentingVC dismissViewControllerAnimated:YES completion:^{
            MFMailComposeViewController *mailController = 
                [[MFMailComposeViewController alloc] init];
            if([MFMailComposeViewController canSendMail]){
                if(mailController) {
                    NSLog(@"%@", self); // This returns InfoController 
                    mailController.mailComposeDelegate = presentingVC; //edited
                    [mailController setSubject:@"I have an issue"];
                    [mailController setMessageBody:@"My issue is ...." isHTML:YES];
                    [presentingVC presentViewController:mailController 
                                               animated:YES 
                                             completion:nil];
                    }
                }
        }];
    }
    

    update/edit
    If you put all of the code in the completion block, you should set mailController's mailComposeDelegate to presentingVC, not self. Then handle the delegate method in the presenting viewController.

    update 2
    @Auro has provided a detailed solution using a delegate method, and in his comments points out that this best expresses the separation of roles. The traditionalist in me agrees, and I do regard dismissViewController:animated:completion as a kludgy and easily misunderstood piece of API.

    Apple's docs have this to say:

    Dismissing a Presented View Controller

    When it comes time to dismiss a presented view controller, the preferred approach is to let the presenting view controller dismiss it. In other words, whenever possible, the same view controller that presented the view controller should also take responsibility for dismissing it. Although there are several techniques for notifying the presenting view controller that its presented view controller should be dismissed, the preferred technique is delegation. For more information, see “Using Delegation to Communicate with Other Controllers.”

    Notice that they don't even mention dismissViewController:animated:completion: here, as if they don't have much respect for their own API.

    But delegation seems to be an issue that people often struggle with: and can require a lengthy answer... I think this is one of the reasons Apple pushes blocks so hard. In cases where code only needs to execute in one place, the delegate pattern is often regarded by the uninitiated as an overly complex solution to an apparently simple problem.

    I suppose the best answer, if you are learning this stuff, is to implement it both ways. Then you will really get a grip on the design patterns in play.

    0 讨论(0)
  • 2020-12-29 13:36

    Use this code

    [[[[[UIApplication sharedApplication] delegate] window] rootViewController] presentViewController:composer                                                                                                    animated:YES completion:nil];
    

    Instead of

    [self presentViewController:picker animated:YES completion:NULL];
    
    0 讨论(0)
提交回复
热议问题