I have REAL misunderstanding with MFMailComposeViewController in Swift (iOS8) in Simulator
Asked Answered
A

7

56

I create a CSV file and try to send it by e-mail. Displays a window to send mail, but is not filled with the body of the email, and no attached file. Application hangs with this screen:

prntscr.com/4ikwwm

button "Cancel" does not work. After a few seconds in the console appears:

viewServiceDidTerminateWithError: Error Domain=_UIViewServiceInterfaceErrorDomain Code=3 "The operation couldn’t be completed. (_UIViewServiceInterfaceErrorDomain error 3.)" UserInfo=0x7f8409f29b50 {Message=Service Connection Interrupted}

<MFMailComposeRemoteViewController: 0x7f8409c89470> timed out waiting for fence barrier from com.apple.MailCompositionService

There is my code:

func actionSheet(actionSheet: UIActionSheet!, clickedButtonAtIndex buttonIndex: Int) {
    if buttonIndex == 0 {
        println("Export!")

        var csvString = NSMutableString()
        csvString.appendString("Date;Time;Systolic;Diastolic;Pulse")

        for tempValue in results {     //result define outside this function

            var tempDateTime = NSDate()
            tempDateTime = tempValue.datePress
            var dateFormatter = NSDateFormatter()
            dateFormatter.dateFormat = "dd-MM-yyyy"
            var tempDate = dateFormatter.stringFromDate(tempDateTime)
            dateFormatter.dateFormat = "HH:mm:ss"
            var tempTime = dateFormatter.stringFromDate(tempDateTime)

            csvString.appendString("\n\(tempDate);\(tempTime);\(tempValue.sisPress);\(tempValue.diaPress);\(tempValue.hbPress)")
        }

        let fileManager = (NSFileManager.defaultManager())
        let directorys : [String]? = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory,NSSearchPathDomainMask.AllDomainsMask, true) as? [String]

        if ((directorys) != nil) {

            let directories:[String] = directorys!;
            let dictionary = directories[0];
            let plistfile = "bpmonitor.csv"
            let plistpath = dictionary.stringByAppendingPathComponent(plistfile);

            println("\(plistpath)")

            csvString.writeToFile(plistpath, atomically: true, encoding: NSUTF8StringEncoding, error: nil)

            var testData: NSData = NSData(contentsOfFile: plistpath)

            var myMail: MFMailComposeViewController = MFMailComposeViewController()

            if(MFMailComposeViewController.canSendMail()){

                myMail = MFMailComposeViewController()
                myMail.mailComposeDelegate = self

                // set the subject
                myMail.setSubject("My report")

                //Add some text to the message body
                var sentfrom = "Mail sent from BPMonitor"
                myMail.setMessageBody(sentfrom, isHTML: true)

                myMail.addAttachmentData(testData, mimeType: "text/csv", fileName: "bpmonitor.csv")

                //Display the view controller
                self.presentViewController(myMail, animated: true, completion: nil)
            }
            else {
                var alert = UIAlertController(title: "Alert", message: "Your device cannot send emails", preferredStyle: UIAlertControllerStyle.Alert)
                alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.Default, handler: nil))
                self.presentViewController(alert, animated: true, completion: nil)


            }
        }
        else {
            println("File system error!")
        }
    }
}

Trying instead to send mail using UIActivityViewController:

let fileURL: NSURL = NSURL(fileURLWithPath: plistpath)
let actViewController = UIActivityViewController(activityItems: [fileURL], applicationActivities: nil)
self.presentViewController(actViewController, animated: true, completion: nil)

See approximately the same screen to send e-mail, which after a while returning to the previous screen. In the console, now another error:

viewServiceDidTerminateWithError: Error Domain=_UIViewServiceInterfaceErrorDomain Code=3 "The operation couldn’t be completed. (_UIViewServiceInterfaceErrorDomain error 3.)" UserInfo=0x7faab3296ad0 {Message=Service Connection Interrupted}
Errors encountered while discovering extensions: Error Domain=PlugInKit Code=13 "query cancelled" UserInfo=0x7faab3005890 {NSLocalizedDescription=query cancelled}
<MFMailComposeRemoteViewController: 0x7faab3147dc0> timed out waiting for fence barrier from com.apple.MailCompositionService

There was something about PlugInKit.

Trying instead UIActivityViewController using UIDocumentInteractionController:

let docController = UIDocumentInteractionController(URL: fileURL)
docController.delegate = self
docController.presentPreviewAnimated(true)
...

func documentInteractionControllerViewControllerForPreview(controller: UIDocumentInteractionController!) -> UIViewController! {
    return self
}

I see this screen with contents a CSV-file:

enter image description here

I press button export in top-right and see this screen:

enter image description here

where I choose MAIL and for several seconds I see:

enter image description here

Then returns to displaying the contents of the file! In the console the same messages as when using UIActivityViewController.

Avarice answered 1/9, 2014 at 11:12 Comment(0)
J
118

* * IMPORTANT - DO NOT USE THE SIMULATOR FOR THIS. * *

Even in 2016, the simulators very simply do not support sending mail from apps.

Indeed, the simulators simply do not have mail clients.

But! Do see the message at the bottom!


Henri has given the total answer. You MUST

-- allocate and initiate MFMailComposeViewController in an earlier stage, and

-- hold it in one static variable, and then,

-- whenever it's needed, get the static MFMailComposeViewController instance and use that.

AND you will almost certainly have to cycle the global MFMailComposeViewController after each use. It is not reliable to re-use the same one.

Have a global routine which releases and then re-initializes the singleton MFMailComposeViewController. Call to that global routine, each time, after you are finished with the mail composer.

Do it in any singleton. Don't forget that your app delegate is, of course, a singleton, so do it there...

@property (nonatomic, strong) MFMailComposeViewController *globalMailComposer;

-(BOOL)application:(UIApplication *)application
   didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
    {
    ........
    // part 3, our own setup
    [self cycleTheGlobalMailComposer];
    // needed due to the worst programming in the history of Apple
    .........
    }

and...

-(void)cycleTheGlobalMailComposer
    {
    // cycling GlobalMailComposer due to idiotic iOS issue
    self.globalMailComposer = nil;
    self.globalMailComposer = [[MFMailComposeViewController alloc] init];
    }

Then to use the mail, something like this ...

-(void)helpEmail
    {
    // APP.globalMailComposer IS READY TO USE from app launch.
    // recycle it AFTER OUR USE.
    
    if ( [MFMailComposeViewController canSendMail] )
        {
        [APP.globalMailComposer setToRecipients:
              [NSArray arrayWithObjects: emailAddressNSString, nil] ];
        [APP.globalMailComposer setSubject:subject];
        [APP.globalMailComposer setMessageBody:msg isHTML:NO];
        APP.globalMailComposer.mailComposeDelegate = self;
        [self presentViewController:APP.globalMailComposer
             animated:YES completion:nil];
        }
    else
        {
        [UIAlertView ok:@"Unable to mail. No email on this device?"];
        [APP cycleTheGlobalMailComposer];
        }
    }

-(void)mailComposeController:(MFMailComposeViewController *)controller
     didFinishWithResult:(MFMailComposeResult)result
     error:(NSError *)error
    {
    [controller dismissViewControllerAnimated:YES completion:^
        { [APP cycleTheGlobalMailComposer]; }
        ];
    }

{nb, fixed typo per Michael Salamone below.}

Have the following macro in your Prefix file for convenience

#define APP ((AppDelegate *)[[UIApplication sharedApplication] delegate])

Also here's a "minor" problem which can cost you days: https://mcmap.net/q/347007/-meaning-of-warning-quot-while-a-presentation-is-in-progress-quot


Just for 2016 FTR here's the basic swift code to send an email IN APP,

class YourClass:UIViewController, MFMailComposeViewControllerDelegate
 {
    func clickedMetrieArrow()
        {
        print("click arrow!  v1")
        let e = MFMailComposeViewController()
        e.mailComposeDelegate = self
        e.setToRecipients( ["[email protected]"] )
        e.setSubject("Blah subject")
        e.setMessageBody("Blah text", isHTML: false)
        presentViewController(e, animated: true, completion: nil)
        }
    
    func mailComposeController(controller: MFMailComposeViewController, didFinishWithResult result: MFMailComposeResult, error: NSError?)
        {
        dismissViewControllerAnimated(true, completion: nil)
        }

However! Note!

These days it is crappy to send an email "in app".

It's much better today to simply cut away to the email client.

Add to plist ...

<key>LSApplicationQueriesSchemes</key>
 <array>
    <string>instagram</string>
 </array>

and then code like

func pointlessMarketingEmailForClient()
    {
    let subject = "Some subject"
    let body = "Plenty of <i>email</i> body."
    
    let coded = "mailto:[email protected]?subject=\(subject)&body=\(body)".stringByAddingPercentEncodingWithAllowedCharacters(.URLQueryAllowedCharacterSet())
    
    if let emailURL:NSURL = NSURL(string: coded!)
        {
        if UIApplication.sharedApplication().canOpenURL(emailURL)
            {
            UIApplication.sharedApplication().openURL(emailURL)
            }
        else
            {
            print("fail A")
            }
        }
    else
        {
        print("fail B")
        }
    }

These days, that is much better than trying to email from "inside" the app.

Remember again the iOS simulators simply do not have email clients (nor can you send email using the composer within an app). You must test on a device.

Jellyfish answered 16/9, 2014 at 8:38 Comment(21)
Thanks for expanding on my answer :) . Also it's important to point out that one should cycle the mail composer JUST BEFORE reusing it. This saves many headaches with crashes due to delegate methods. The idea is to always keep the mail composer happily alive until you need to reuse it. At that point, you recycle it.Dosia
... Anyways I am very interested to hear your report that you found it better to cycle just BEFORE !! using it. Like I say strangely we found it better to cycle after!! (What a shambles from Apple, just to send email!) BTW did you notice the various reports from people that "custom fonts" could affect the issue. I never did track that down. You rock, cordialementJellyfish
I've found that the only thing that worked for me was to make sure that the latest mail composer used was around all the time, until I specifically needed it again. At that point I recycle and do my thing. But the whole thing is so buggy that I'm not surprised you have a different way of doing it. Oh, and I also use custom fonts.Dosia
Thanks for your help but it still doesn't work for me, at least not in the iOS8 simulators. The mail composer only displays de subject (not the body) and then closes after a while (or immediately, sometimes) without doing anything.Formless
So in conclusion, recycle it right AFTER you're done with it, and a second time right BEFORE you're ready to use it again. Perfect ;)Soho
Hey @Ferran - you know, on this issue I wouldn't even bother trying the simulators, unfortunately! :( That being said, it's just possible that that particular problem relates to this critical factoid: https://mcmap.net/q/347007/-meaning-of-warning-quot-while-a-presentation-is-in-progress-quotJellyfish
Hey @Soho -- you know it sounds like a joke right! Really. But, incredibly, just FWIW we found that doing just what you say .. multi-cycling! - did not work. Just for us in that project, we could not reliably cycle "just before" using. Note that indeed Henri explains the opposite experience. I tried also a manual recycle button for development (like, recycle .. manually wait .. use mail) with mixed results!!!!Jellyfish
Hi Joe Blow, firstly thanks for your workaround. Did you manage to get this working on the iOS 8 simulator or only on a physical device? I have implemented your solution but still having the issue with the simulator, and I don't have access to a device right now. I can confirm the solution works on the iOS 7.1 simulator though, but I can't get it working for iOS 8.Tybalt
I'm sorry but what if I have UIActivityViewController which creates mail composer for me? How should I fix this?Striptease
Hey stas - it's very likely you just cannot use UIActivityViewController! the api is a bit of a dog. I would suggest, as a new question on that. Hopefully someone will know.Jellyfish
Very interested in how to use this workaround with UIActivityViewController as well. This is driving me nuts!Reminiscent
It'd be great if this answer (as the selected answer) was updated to include something about this all being broken in the iOS 8 simulator. I've just wasted many hours, before coming back here, expanding these comments, and finding out that it won't work in the simulator.Hypertrophy
Wish I would have read this first!!! Just spent a couple of days trying to make all this work via Swift on a simulator == totalBrainDamage! Arrrggghhhhh!!! Turns out you don't need to use a singleton ... at least on 8.1.1 you can spool up either a UIActivityViewController or a MFMailComposeViewController as needed/wanted and all seems to work perfectly fine. Thanks JB.Oloughlin
Thank God it helped! Merry Xmas!Jellyfish
@JoeBlow Would recommend updating since a lot of people see this. You shouldn't be dismissing in mailComposeController:didFinishWithResult:error: using controller you should dismiss from the VC that did the presentation [self dismissViewControllerAnimated:YES completion:nil]. This is standard and the docs recommend it as well. developer.apple.com/library/ios/documentation/MessageUI/…Leukas
So, I can't really think of any particularly reason what for this class it must be dismissed "from itself" ... testing shows no difference at all, as you'd expect. Perhaps someone else has some info or thoughts on this?Jellyfish
I think it might be a good idea to check the "canSendEmail" before calling the cycle at init. Otherwise, all the users that are using GMail app or whatever will get the error at INIT.Basle
Dismissing the singleton (if it exists) should be done right before presenting it. It's that simple. It keeps with the standards of Apple, and more importantly it ensures that the stupid thing stays around as long as necessary for whatever depends on it.Dosia
Hi Rikkles - run that by me again, did you mean dismissing the singleton or something else? (I don't know how to dismiss a singleton - may misunderstand you. A singleton is just like for example AppDelegate itself - you can't ever stop, destroy, or anything that singleton.Jellyfish
Even today in 2016, it doesn't work... Really Apple?!Unprofessional
Quote: "needed due to the worst programming in the history of Apple". +1 especially for thatHang
D
17

It has nothing to do with Swift. It's an issue with the mail composer that's been around forever it seems. That thing is extremely picky, from failing with timeouts to sending delegate messages even when cancelled.

The workaround everyone uses is to create a global mail composer (for example in a singleton), and every single time reinitializing it when you need it. This ensures the mail composer is always around when the OS needs it, but also that it is free of any crap when you want to reuse it.

So create a strong (as global as possible) variable holding the mail composer and reset it every time you want to use it.

Dosia answered 1/9, 2014 at 11:35 Comment(6)
I have the same problem - only on iOS8. And my code is ObjC, so it's not Swift to blame.Whitefly
Hm, just tried it on hardware and it works fine. Seems it's an issue only in the iOS8 simulator.Whitefly
@AXE, watch out, the problem is still there in 7 even 8. Note it is only "erratic" (depends on other stuff) so you won't see it every time. We all just have to use MFMailComposeViewController in a static, that's all there is to it (and you must cycle it). They should just put that in the doco. Fortunately, it's not hard to do :-OJellyfish
I can reproduce it only on iOS8 only on Simulator. Also, shouldn't this problem be present only AFTER the first time you try to use the Mail Composer? In my case it doesn't open even the first time.Whitefly
Same problem with iOS 9.2 simulatorOnto
People, do NOT try to test it on the simulator. It fails randomly. You can only verify it on the device itself.Dosia
T
6
  • XCode 6 Simulator has problems managing Mailcomposer and other things.
  • Try testing the code with a real device. Likely it will work.
  • I have problems when running MailComposer from actionSheet button, also with real test. With IOS 7 worked fine, the same code in IOS 8 does not work. For me Apple must depurated the XCode 6. ( too many different simulated devices with Objective-C and Swift together ...)
Tonneau answered 26/9, 2014 at 9:58 Comment(0)
H
1

Not sure if the recycling proposed in above solution is necessary or not. But you do need use proper parameters.

The delegate receives a MFMailComposeViewController* parameter. And you need to use that instead of self when dismissing the controller. I.e.

The delegate receives the (MFMailComposeViewController *) controller. And you need to use that instead of self when dismissing the MFMailComposeViewController controller. That is what you want to dismiss after all.

-(void)mailComposeController:(MFMailComposeViewController *)controller
     didFinishWithResult:(MFMailComposeResult)result
     error:(NSError *)error
    {
    [controller dismissViewControllerAnimated:YES completion:^
        { [APP cycleTheGlobalMailComposer]; }
        ];
    }
Helping answered 14/11, 2014 at 16:17 Comment(3)
I don't agree with this. You shouldn't be dismissing using controller, you should dismiss from the VC self that did the presentation [self dismissViewControllerAnimated:YES completion:nil]. This is standard and the docs recommend it as well. developer.apple.com/library/ios/documentation/MessageUI/…Leukas
Hi Michael - just TBC thanks for pointing out the typo in my code. For anyone reading this now, I had accidentally typed "self dismiss..." rather than "controller dismiss..." Naturally Michael is 10000% correct. Note that if you are reading this "now" I already changed it in the answer above also. Thanks again Michael!Jellyfish
I don't think sending dismiss message to self or controller makes any change as documentation states: The presenting view controller is responsible for dismissing the view controller it presented. If you call this method on the presented view controller itself, UIKit asks the presenting view controller to handle the dismissal.Baty
C
1

Create a property for the mail composer and instantiate it in view did load than call it when ever you need a mail composer.

@property (strong, nonatomic) MFMailComposeViewController *mailController;
self.mailController = [[MFMailComposeViewController alloc] init];
[self presentViewController:self.mailController animated:YES completion:^{}];
Camion answered 2/12, 2014 at 21:17 Comment(0)
F
1

Hey this is solved with iOS 8.3 released 2 days ago.

Fluctuate answered 15/4, 2015 at 13:20 Comment(2)
Build it in 8.4. Still same!Hepza
Still the same in 9.3Bettinabettine
A
1

A simple helper class for handling mail in Swift. Based on Joe Blow's answer.

import UIKit
import MessageUI

public class EmailManager : NSObject, MFMailComposeViewControllerDelegate
{
    var mailComposeViewController: MFMailComposeViewController?

    public override init()
    {
        mailComposeViewController = MFMailComposeViewController()
    }

    private func cycleMailComposer()
    {
        mailComposeViewController = nil
        mailComposeViewController = MFMailComposeViewController()
    }

    public func sendMailTo(emailList:[String], subject:String, body:String, fromViewController:UIViewController)
    {
        if MFMailComposeViewController.canSendMail() {
            mailComposeViewController!.setSubject(subject)
            mailComposeViewController!.setMessageBody(body, isHTML: false)
            mailComposeViewController!.setToRecipients(emailList)
            mailComposeViewController?.mailComposeDelegate = self
            fromViewController.presentViewController(mailComposeViewController!, animated: true, completion: nil)
        }
        else {
            print("Could not open email app")
        }
    }

    public func mailComposeController(controller: MFMailComposeViewController, didFinishWithResult result: MFMailComposeResult, error: NSError?)
    {
        controller.dismissViewControllerAnimated(true) { () -> Void in
            self.cycleMailComposer()
        }
    }
}

Place as instance variable in AppDelegate-class and call when needed.

Actualize answered 9/12, 2015 at 13:22 Comment(5)
Thanks for doing this! When you say place instance in SharedDelegate-class, do you mean the AppDelegate?Celesta
When I try to initialize, I get an error saying "fatal error: unexpectedly found nil while unwrapping an Optional value". I'm doing ` emailer.sendMailTo(["[email protected]"], subject: "Hi!", body: "Hi", fromViewController: self)`Celesta
Which row is it failing on? Which variable is nil? Make sure that your emailer is initialized before using it. I initialize my AppDelegate instance variables in the didFinishLaunchingWithOptions methodActualize
I just added emailer = EmailManager.init() to didFinishLaunchingWithOptions and I can see the mailComposeViewController launch but then I get the same error "viewServiceDidTerminateWithError: Error Domain=_UIViewServiceInterfaceErrorDomain Code=3 "(null)" UserInfo={Message=Service Connection Interrupted}"Celesta
I hope you are not using a simulator. As mentioned above among other places the simulator does not work with opening emails. emailer = EmailManager() should also be enough in Swift.Actualize

© 2022 - 2024 — McMap. All rights reserved.