Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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

I am attempting to use dismissViewControllerAnimated:animated:completion and presentViewControllerAnimated:animated:completion in succession. Using a storyboard, I am modally presenting InfoController via a partial curl animation.

The partial curl reveals a button on InfoController that I want to initiate the MFMailComposeViewController. Because the partial curl partially hides the MFMailComposeViewController, I first want to dismiss InfoController by un-animating the partial curl. Then I want the MFMailComposeViewController to animate in.

At present, when I try this, the partial curl un-animates, but the MFMailComposeViewController doesn't get presented. I also have a warning of:

Warning: Attempt to present MFMailComposeViewController: on InfoController: whose view is not in the window hierarchy!

InfoController.h:

#import <UIKit/UIKit.h>
#import <MessageUI/MessageUI.h>
#import <MessageUI/MFMailComposeViewController.h>

@interface InfoController : UIViewController <MFMailComposeViewControllerDelegate>

@property (weak, nonatomic) IBOutlet UIButton *emailMeButton;

-(IBAction)emailMe:(id)sender;

@end

InfoController.m

#import "InfoController.h"

@interface InfoController ()

@end

@implementation InfoController 

- (void)viewDidLoad
{
    [super viewDidLoad];
}

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

- (void)sendMeMail {
MFMailComposeViewController *mailController = [[MFMailComposeViewController alloc] init];
if([MFMailComposeViewController canSendMail]){
    if(mailController)
    {
        NSLog(@"%@", self); // This returns InfoController 
        mailController.mailComposeDelegate = self;
        [mailController setSubject:@"I have an issue"];
        [mailController setMessageBody:@"My issue is ...." isHTML:YES];
        [self presentViewController:mailController animated:YES completion:nil];
    }
}
}

- (void)mailComposeController:(MFMailComposeViewController*)controller
          didFinishWithResult:(MFMailComposeResult)result
                        error:(NSError*)error;
{
    if (result == MFMailComposeResultSent) {
        NSLog(@"It's sent!");
    }
    [self dismissViewControllerAnimated:YES completion:nil];
}

Also, if I comment out the [self dismissViewControllerAnimated:YES completion:^{}]; in (IBAction)emailMe, the MFMailComposeViewController animates in but it is partially hidden behind the partial curl. How can I first dismiss the curl and then animate in the MFMailComposeViewController?

Thanks very much!

Edit: Below image of what the view looks like if I comment out [self dismissViewControllerAnimated:YES completion:^{}];

enter image description here

like image 261
dianna Avatar asked May 05 '13 11:05

dianna


People also ask

What is a UIViewController?

The UIViewController class defines the shared behavior that's common to all view controllers. You rarely create instances of the UIViewController class directly. Instead, you subclass UIViewController and add the methods and properties needed to manage the view controller's view hierarchy.


3 Answers

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
like image 58
auco Avatar answered Nov 07 '22 10:11

auco


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.

like image 42
foundry Avatar answered Nov 07 '22 10:11

foundry


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];
    }];

}
like image 1
Thilina Chamath Hewagama Avatar answered Nov 07 '22 12:11

Thilina Chamath Hewagama