我在模拟器中使用Swift(iOS8)的MFMailComposeViewController遇到了真正的困惑。

56
我创建了一个CSV文件并尝试通过电子邮件发送它。弹出一个发送邮件的窗口,但是没有填写电子邮件正文,也没有附加文件。应用程序卡在此屏幕上:

prntscr.com/4ikwwm

“取消”按钮无法工作。几秒钟后,在控制台中出现以下内容:
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

这是我的代码:

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!")
        }
    }
}

尝试使用UIActivityViewController发送邮件:

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

看到大约相同的屏幕发送电子邮件,一段时间后返回到以前的屏幕。在控制台中,现在又出现了另一个错误:

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

关于 PlugInKit 有一些内容。

尝试使用 UIDocumentInteractionController 而不是 UIActivityViewController

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

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

我看到了一个包含CSV文件内容的屏幕:

输入图像描述

我点击右上方的导出按钮,然后看到这个屏幕:

输入图像描述

在这里我选择了邮件,并且几秒钟后我看到:

输入图像描述

然后回到显示文件内容的界面!在控制台中,与使用UIActivityViewController时相同的消息。

7个回答

118

* * 重要 - 不要使用模拟器。* *

即使在2016年,模拟器非常简单,也不支持从应用程序发送邮件。

事实上,模拟器根本没有邮件客户端。

但是!请看底部的消息!


Henri已经给出了完整的答案。你必须

-- 在较早的阶段分配和初始化MFMailComposeViewController,并且

-- 将其保存在一个静态变量中,然后,

-- 每当需要时,获取静态MFMailComposeViewController实例并使用它。

并且几乎肯定每次使用后都需要循环全局的MFMailComposeViewController。重新使用同一个是不可靠的。

编写一个全局例程,释放然后重新初始化单例MFMailComposeViewController。在完成邮件创作后,每次调用该全局例程。

可以在任何单例中执行此操作。不要忘记你的应用程序委托是一个单例,所以你可以在那里执行此操作...

@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
    .........
    }

而且...

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

然后使用邮件,类似这样的方式...

-(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,已根据Michael Salamone的建议更正了拼写错误。}

为方便起见,在您的前缀文件中加入以下宏

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

这里还有一个“小”问题,可能会浪费您好几天的时间:https://dev59.com/l2Yq5IYBdhLWcg3weQgy#17120065


仅供参考,在2016年,以下是发送应用内邮件的基本 Swift 代码:

class YourClass:UIViewController, MFMailComposeViewControllerDelegate
 {
    func clickedMetrieArrow()
        {
        print("click arrow!  v1")
        let e = MFMailComposeViewController()
        e.mailComposeDelegate = self
        e.setToRecipients( ["help@smhk.com"] )
        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)
        }

然而!注意!

现在发送“应用内”邮件已经很糟糕了。

今天更好的方法是直接切换到电子邮件客户端。

添加到plist...

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

然后像这样编写代码

func pointlessMarketingEmailForClient()
    {
    let subject = "Some subject"
    let body = "Plenty of <i>email</i> body."
    
    let coded = "mailto:blah@blah.com?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")
        }
    }

现在,这比从应用程序“内部”尝试发送电子邮件要好得多。

请记住,iOS模拟器根本没有电子邮件客户端(也不能使用应用程序中的组合器发送电子邮件)。您必须在设备上进行测试。


1
感谢您对我的回答进行了扩展 :)。此外,重要的是要指出,在重新使用邮件组合器之前,应该循环使用它。这可以避免由于委托方法而导致的崩溃问题。理念是始终保持邮件组合器的正常运行,直到需要重新使用它为止。在那时,您可以对其进行回收利用。 - Rikkles
1
我发现对我有效的唯一方法是确保最新的邮件撰写器一直存在,直到我再次需要它。在那时,我会回收利用并继续操作。但整个过程非常有漏洞,所以我不奇怪你有另一种处理方式。哦,我还使用自定义字体。 - Rikkles
1
如果这个答案(作为被选中的答案)能够更新,加入一些关于在iOS 8模拟器中出现问题的内容,那将会非常好。我刚刚浪费了很多时间,在回到这里之前,扩展了这些评论,发现它在模拟器中无法工作。 - Stewart Macdonald
1
在调用初始化时,我认为先检查“canSendEmail”可能是个好主意。否则,所有使用GMail应用程序或其他应用程序的用户都将在INIT时收到错误。 - Lucas
1
“由于苹果历史上最糟糕的编程,这是必需的。” +1 特别是为此。 - Olivier Pons
显示剩余16条评论

17

这与Swift无关,而是与邮件组合器有关,似乎已经存在了很长时间。这个东西非常挑剔,从超时失败到即使被取消也发送委托消息。

每个人都使用的解决方法是创建一个全局的邮件组合器(例如在单例中),并且每次需要时重新初始化它。这确保了邮件组合器始终存在于操作系统需要它的情况下,但当您想要重用它时它也是干净的。

因此,创建一个强变量(尽可能全局)来保存邮件组合器,并在每次使用时重新设置它。


3
我有同样的问题-只发生在iOS8上。我的代码是ObjC,所以不能怪Swift。 - AXE
2
嗯,在硬件上试过了,运行良好。看起来这只是在iOS8模拟器中出现的问题。 - AXE
@AXE,注意了,问题在7甚至8中仍然存在。请注意它只是“不稳定的”(取决于其他因素),所以您不会每次都看到它。我们只需要在静态中使用MFMailComposeViewController,这就是全部内容(并且您必须对其进行循环)。他们应该把这放在文档中。幸运的是,这并不难做 :-O - Fattie
我只能在iOS8模拟器上重现它。 而且,这个问题不应该只出现在第一次尝试使用邮件组合器之后吗? 但在我的情况下,即使是第一次也无法打开。 - AXE
iOS 9.2模拟器也有同样的问题。 - JOM
大家,不要试图在模拟器上测试它。它会随机失败。你只能在设备本身上验证它。 - Rikkles

6
  • XCode 6模拟器在管理Mailcomposer和其他事物方面存在问题。
  • 尝试使用真实设备进行测试。很可能会正常工作。
  • 当我从actionSheet按钮运行MailComposer时,也遇到了问题,实际测试也是如此。在IOS 7中工作得很好,在IOS 8中相同的代码不起作用。对我来说,苹果必须调试XCode 6。(使用Objective-C和Swift一起有太多不同的模拟设备...)

1
在viewDidLoad中创建一个邮件composer属性,并在需要邮件composer时调用它。
@property (strong, nonatomic) MFMailComposeViewController *mailController;
self.mailController = [[MFMailComposeViewController alloc] init];
[self presentViewController:self.mailController animated:YES completion:^{}];

1

这是一个处理Swift邮件的简单辅助类。基于Joe Blow的答案。

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()
        }
    }
}

将其作为实例变量放置在AppDelegate类中,并在需要时调用。


谢谢您做这件事!当您说在SharedDelegate类中放置实例时,您是指AppDelegate吗? - Brosef
当我尝试初始化时,出现错误提示“fatal error: unexpectedly found nil while unwrapping an Optional value”。我的代码是emailer.sendMailTo(["test@test.com"], subject: "Hi!", body: "Hi", fromViewController: self) - Brosef
它在哪一行失败了?哪个变量是nil?确保在使用之前初始化您的emailer。我在didFinishLaunchingWithOptions方法中初始化我的AppDelegate实例变量。 - Sunkas
我刚刚在 didFinishLaunchingWithOptions 中添加了 emailer = EmailManager.init(),我可以看到 mailComposeViewController 启动,但然后我收到相同的错误信息 "viewServiceDidTerminateWithError: Error Domain=_UIViewServiceInterfaceErrorDomain Code=3 "(null)" UserInfo={Message=Service Connection Interrupted}"。 - Brosef
希望你没有使用模拟器。如上所述,模拟器在打开电子邮件时无法正常工作。在Swift中,“emailer = EmailManager()”也应该足够了。 - Sunkas

1
不确定上述解决方案中提出的回收是否必要。但您需要使用适当的参数。委托接收一个MFMailComposeViewController*参数。在解除控制器时,您需要使用该参数而不是self。即,委托接收(MFMailComposeViewController*)控制器。在解除MFMailComposeViewController控制器之后,您需要使用它。
-(void)mailComposeController:(MFMailComposeViewController *)controller
     didFinishWithResult:(MFMailComposeResult)result
     error:(NSError *)error
    {
    [controller dismissViewControllerAnimated:YES completion:^
        { [APP cycleTheGlobalMailComposer]; }
        ];
    }

1
我不同意这个。你不应该忽视使用 controller,你应该从VC self中解雇执行 [self dismissViewControllerAnimated:YES completion:nil] 的操作。这是标准做法,文档也推荐这样做。 https://developer.apple.com/library/ios/documentation/MessageUI/Reference/MFMailComposeViewControllerDelegate_protocol/ - Jesse
嗨,Michael - 只是 TBC 感谢您指出我的代码中的拼写错误。对于现在正在阅读此内容的任何人,我不小心输入了 "self dismiss..." 而不是 "controller dismiss..." 当然,Michael 是正确的。请注意,如果您现在正在阅读此内容,则我已经在上面的答案中进行了更改。再次感谢你,Michael! - Fattie
我认为向自身或控制器发送解散消息不会产生任何变化,因为文档说明如下:“呈现视图控制器负责解除其呈现的视图控制器。如果您在呈现的视图控制器本身上调用此方法,则UIKit会要求呈现视图控制器处理解除。” - ugur

1

嘿,这个问题已经在两天前发布的iOS 8.3中得到解决。


在8.4版本中构建。仍然一样! - Jamil
仍然是9.3版本中的相同内容。 - mkll

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接