presentModalViewController crashes my app

2019-02-15 14:44发布

问题:

It's one of the simplest things to do, I know. But I've been banging my head against this for days. I've done it plenty of times in the past, but for some reason trying to present a modal view controller just crashes the app to a black screen. Nothing reported in the console or anything. I'm hoping someone might have had this problem and has some advice.

This code is called from a UIViewController class:

MFMailComposeViewController *controller = [[MFMailComposeViewController alloc] init];
controller.mailComposeDelegate = self;
[controller setSubject:@"test subject"];
[controller setMessageBody:@"this is the message body" isHTML:NO];
[self presentModalViewController:controller animated:YES];

回答1:

As Andrew has pointed out in his comment, do you check

+[MFMailComposeViewController canSendMail]

before trying to push the view controller? The behavior of the MFMailComposeViewController is not well defined if this method returns NO (which may also well be the case when running on the simulator, though I'm not sure). From the documentation:

Before using this class, you must always check to see if the current device is configured to send email at all using the canSendMail method. If the user’s device is not set up for the delivery of email, you can notify the user or simply disable the email dispatch features in your application. You should not attempt to use this interface if the canSendMail method returns NO.

Have you tried to push another view controller instead? Does this crash your app, too?



回答2:

Are you showing another modal view controller before trying to show MFMailComposeViewController? I had the same problem and found a workaround:

- (void)peopleMultiPickerNavigationController:(PeopleMultiPickerNavigationController *)peoplePicker 
                                didSelectContacts:(NSArray *)contacts {

[self dismissModalViewControllerAnimated:YES];

// some more code here

[self performSelector:@selector(sendEmail) withObject:nil afterDelay:0.45]; // this works only if delay > ~0.4!
// [self sendEmail]; // this won't work

// some more code here

}

- (void) sendEmail {
  Class mailClass = (NSClassFromString(@"MFMailComposeViewController"));
  if (mailClass != nil) {
    // We must always check whether the current device is configured for sending emails
    if ([mailClass canSendMail]) {
      [self displayComposerSheet:emails];
    } else {
      [self launchMailAppOnDevice:emails];
    }
  } else {
    [self launchMailAppOnDevice:emails];
  } 
}

I know it's an ugly workaround, but I didn't found anything better :(



回答3:

I don't know how relevant this is, but I have been having horrible problems trying to present the MFMailComposeViewController after returning to my main view controller from ANOTHER modal view controller. It just would not work. I was trying to dismiss this other modal view controller before launching the mail controller, and found my solution to be to not call:

[self dismissModalViewControllerAnimated:YES];

But to instead call:

[self dismissModalViewControllerAnimated:NO];

Then go ahead and present the mail view controller.

That one change made all the difference in my case. I suspect this is connected to the problem sgosha was having. Just switch off the animation, rather than putting a delay in (which is probably just waiting until the animation has completed). Looks like a bug in the framework to me.

I should probably explain further. I have a share button on my main view controller, which modally pops up a table view to allow to user to choose what they want to share. From here, if they tap on Email, they get a UIActionSheet letting them further decide which files they wish to attach to their email.

It is possible the UIActionSheet in the mix is contributing to the problem. The actual dismissal of the modal view controllers is taking place in the delegate methods back in my main view controller, and it is this main view controller which tries to launch the mail view controller after dismissing the modal table view controller.



回答4:

Yes! I did it! I can not believe, but I solved the problem! That's related to:

Opening an MFMailComposeViewController as modal from (and over) some other opened modal controller

Unfortunately, I have to admit that, again, in times of expensive-like-a-Devil-iPhones5, Apple still forses developers to use old, buggy and not convenient code and components! The great example of that is MFMailComposeViewController.


But now let's follow to more pleasant things.

What do we have:

  • This problem appeared as on iPhone with iOS 5.1 as on Simulator with iOS 6.
  • The core problem was when you are trying to open the email controller as a modal one from another modal controller.
  • [MFMailComposeViewController canSendMail] had absolutely no effect for me - it did not work in both ways (it was crashing with or without email functionality available).
  • [self dismissModalViewControllerAnimated:NO/YES] did not change general sense - it was crashing in both ways, but with a little different behavior.
  • I was trying to use a standard approach with mailComposeDelegate (like '.mailComposeDelegate = self').
  • I was calling the email sending controller from both: common (first-level) controller as well as from modal (second-level) one at the same app.
  • App was not always crashing - sometimes the controller just could not dismiss itself (buttons 'cancel' and 'send' were active, but no actions processed). Dependenly on conditions (who is parent opened the controller, was animation or not on appearing etc).
  • There were also no difference if any email recepients were added or not.

So, what my killed 5 working hours discovered is that seems the delegate is "releases" somehow somewhen. I can suppose that if you are opening the email controller as modal over some other modal controller, that (previously modal) controller is being cleaned by garbage collector or some other way and that way the delegate is being cleaned as well (I have no wish to kill several more hours for detailed digging, so I'd leave that to Apple's conscience).

Anyway, in two words, my solution was

to hold the delegate object somewhere with "strong" reference.

in my case, the owner of that delegate was main view controller class (which is always available in my case, as most of the app logic is working with it). That can be AppDelegate instance also.

It would look like:

@interface SharingTools : NSObject <MFMailComposeViewControllerDelegate>

@property UIViewController* currentParentController;

...

-(void)mailComposeController:(MFMailComposeViewController *)controller didFinishWithResult:(MFMailComposeResult)result error:(NSError *)error
{
    if (error)
    {
        ...
    }
    else
    {
        // I pass it somewhere before calling the email controller dialog opener method,
        // because it's different in case we open the email sender 
        // from first-level controller or second- (the modal one)
        [currentParentController dismissViewControllerAnimated:YES completion:^{

            if (_onResult) {
                    ((void(^)(bool))_onResult)(result == MFMailComposeResultSent);
            }
        }];
    }

    // and of course clearing all the references etc here
    currentParentController.mailComposeDelegate = nil;
    currentParentController = nil;
}

-(void)sendEmail //... params here 
                 // (possibly, including to store also currentParentController)
{
    currentParentController = ... ;
    [currentParentController presentViewController:
                  newlyCreatedEmailController animated:YES completion:nil];
}
@end

@interface MyMainViewController : UIViewController

@property SharingTools* sharing; // initialize somewhere (on viewDidLoad, for instance)

...

-(void)showSettings
{
    ...
    [self presentModalViewController:settingsController animated:YES];
}
@end

@interface SettingsViewController : UIViewController

...

-(void)sendEmailSupport
{
    // again, this is up to you where and how to have either reference 
    // to main controller (or any other owner of the delegate object)
    // or sharing directly
    [myMainViewControllerInstance.sharing sendEmail: ... parentController:self];

}
@end

Frankly saying, the code is kind of messy, but this is just general thoughts. I hope, you'll manage your own much nicer.

Good luck and God bless Microsoft! ^^