9

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

Mukesh Ram
  • 5,814
  • 4
  • 15
  • 34
dianna
  • 156
  • 1
  • 1
  • 5

5 Answers5

19

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
auco
  • 8,812
  • 2
  • 42
  • 53
  • Thanks for your help, auco. This is a really thorough answer and I can tell I'm on the right path. But I'm having some trouble implementing it because I don't see how I can get away with only #import MainController into InfoController.h and not also importing InfoController.h into MainController. If I am to own and have a property of InfoController in MainController, then how can I get around this? I would also need to set the delegate there. – dianna May 06 '13 at 14:58
  • Note: Previously, I did not alloc / init a copy of InfoController in MainController; I simply made a segue from a button in the storyboard. So there is not already a reference to it, as you had assumed in step 4. Thanks again for your help! – dianna May 06 '13 at 14:59
  • sorry for the confusion, there were some really bad mistakes in my text (The concept was right, I just mixed up some names in the first and third step) ...now I actually tested it and it should work that way – auco May 06 '13 at 15:41
  • It does work, indeed! You really made my day. Thanks so much, auco. – dianna May 06 '13 at 18:09
3

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.

Community
  • 1
  • 1
foundry
  • 30,849
  • 8
  • 87
  • 124
  • Thank you, He Was, this is perfect. I tried the second option and the animations are working just the way I wanted. But now I am having trouble dismissing the mailController. Do you have any advice for how to handle the method: - (void)mailComposeController:(MFMailComposeViewController*)controller didFinishWithResult:(MFMailComposeResult)result error:(NSError*)error; – dianna May 06 '13 at 06:34
  • You have a very good answer here, but I don't agree with "protocols are trad now we have blocks", implementing that blocks should be used instead of protocols. While you can use blocks that way (if you know what you're doing), I'd consider it not the best practice in this specific case. Apple docs to dismissViewControllerAnimated say: "The presenting view controller is responsible for dismissing the view controller it presented". ...see my own answer about protocols and why I think it's important to define a clear relationship with clear responsibilities of presenting VCs. – auco May 06 '13 at 11:04
  • @doubledi, see my code edit. That method should be handled in your presenting viewController. If using the second solution (all code inside block) you need to change the reference from `self` to `presentingVC` – foundry May 06 '13 at 12:40
  • @auco, thanks for your input, and +1 for taking the time to provide the delegate solution. See my updated answer.. – foundry May 06 '13 at 12:53
1

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

}
Thilina Hewagama
  • 8,790
  • 2
  • 30
  • 43
  • Thanks very much for your quick response! The second part of your code is very helpful for dismissing both view controllers and going back to my root view controller -- so thank you. However, because I'm using a partial curl animation, the MFMailComposeViewController loads under the partially curled view if I delete the dismissViewControllerAnimated:YES completion:^{}]; See the link to a screen shot in my edit. Any idea how I can fix that? – dianna May 05 '13 at 12:16
0

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.

smileBot
  • 18,797
  • 7
  • 60
  • 62
0

Use this code

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

Instead of

[self presentViewController:picker animated:YES completion:NULL];
Code cracker
  • 2,782
  • 6
  • 31
  • 57
Chandan
  • 1,046
  • 12
  • 30