web-dev-qa-db-fra.com

J'ai REAL malentendu avec MFMailComposeViewController dans Swift (iOS8) dans Simulator

Je crée un fichier CSV et essaie de l'envoyer par courrier électronique. Affiche une fenêtre pour envoyer un courrier, mais ne contient pas le corps du courrier électronique et aucun fichier joint. L'application se bloque avec cet écran:

prntscr.com/4ikwwm

le bouton "Annuler" ne fonctionne pas. Après quelques secondes dans la console apparaît:

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

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

Essayez à la place d'envoyer un courrier en utilisant UIActivityViewController:

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

Voir à peu près le même écran pour envoyer un courrier électronique qui, après un certain temps, revient à l'écran précédent. Dans la console, maintenant une autre erreur:

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

Il y avait quelque chose à propos de PlugInKit.

Essayer à la place UIActivityViewController en utilisant UIDocumentInteractionController:

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

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

Je vois cet écran avec le contenu d'un fichier CSV:

enter image description here

J'appuie sur le bouton export en haut à droite et vois cet écran:

enter image description here

où je choisis MAIL et pendant quelques secondes je vois:

enter image description here

Puis revient à afficher le contenu du fichier! Dans la console, les mêmes messages que lorsque vous utilisez UIActivityViewController.

55
Alexey Nakhimov

* * IMPORTANT - N'UTILISEZ PAS LE SIMULATEUR À CET EFFET. * *

Même en 2016, les simulateurs ne prennent tout simplement pas en charge l'envoi de courrier à partir d'applications.

En effet, les simulateurs ne disposent tout simplement pas de clients de messagerie.

Mais! Ne voir le message en bas!


Henri a donné la réponse totale. Vous devez

- allouer et lancer MFMailComposeViewController à un stade antérieur , et

- maintenez-le dans une variable statique , puis,

- Chaque fois que cela est nécessaire, obtenez l'instance statique MFMailComposeViewController et utilisez-la.

ET vous devrez presque certainement faire un cycle du MFMailComposeViewController global après chaque utilisation. Il est pas fiable de réutiliser le même.

Ayez une routine globale qui libère puis réinitialise le singleton MFMailComposeViewController. Appelez cette routine globale, à chaque fois, une fois que vous avez terminé avec le composeur de courrier.

Faites-le dans n'importe quel singleton. N'oubliez pas que votre délégué aux applications est bien sûr un singleton, alors faites-le là-bas ...

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

et...

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

Ensuite, pour utiliser le courrier, quelque chose comme ça ...

-(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, correction d'une faute de frappe par Michael Salamone ci-dessous.}

Avoir la macro suivante dans votre fichier de préfixe pour plus de commodité

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

Voici également un problème "mineur" qui peut vous coûter des jours: https://stackoverflow.com/a/17120065/294884


Juste pour 2016 FTR, voici la base Swift code pour envoyer un 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)
        }

Toutefois! Remarque!

Ces jours-ci, il est mauvais d'envoyer un email "in app".

Il est beaucoup mieux, aujourd’hui, d’abandonner tout simplement le client de messagerie.

Ajouter à la pliste ...

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

et puis code comme

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

Ces jours-ci, c'est beaucoup mieux que d'essayer d'envoyer un courriel de "l'intérieur" de l'application.

Rappelez-vous que les simulateurs iOS n'ont tout simplement pas de clients de messagerie (vous ne pouvez pas non plus envoyer de courrier électronique à l'aide de composer dans une application). Vous devez tester sur un appareil.

117
Fattie

Cela n'a rien à voir avec Swift. C'est un problème avec le mail composer) qui existe depuis toujours, semble-t-il. Cette chose est extrêmement difficile, de l'échec avec les délais d'attente à l'envoi de messages de délégué même en cas d'annulation.

La solution de contournement que tout le monde utilise consiste à créer un courrier global composer (par exemple dans un singleton)), et à le réinitialiser à chaque fois que vous en avez besoin. Le courrier est ainsi composer est toujours là quand le système d’exploitation en a besoin, mais aussi qu’il est exempt de toute mauvaise merde lorsque vous souhaitez le réutiliser.

Créez donc une variable forte (aussi globale que possible) contenant le courrier composer) et réinitialisez-la à chaque fois que vous souhaitez l’utiliser.

17
Rikkles
  • XCode 6 Simulator a des problèmes de gestion de Mailcomposer et d’autres choses.
  • Essayez de tester le code avec un appareil réel. Cela fonctionnera probablement.
  • J'ai des problèmes lors de l'exécution de MailComposer à partir du bouton actionSheet, également avec un test réel. Avec IOS 7 a bien fonctionné, le même code que dans IOS 8 ne fonctionne pas. Pour moi Apple doit supprimer le code X 6. (trop nombreux appareils simulés avec Objective-C et Swift ensemble ...)
6
Murolau Murolau

Créez une propriété pour le courrier composer) et instanciez-le en vue. Il s'est chargé puis appelé à chaque fois que vous avez besoin d'un composeur de courrier.

@property (strong, nonatomic) MFMailComposeViewController *mailController;
self.mailController = [[MFMailComposeViewController alloc] init];
[self presentViewController:self.mailController animated:YES completion:^{}];
1
Michal Shatz

Pas sûr que le recyclage proposé dans la solution ci-dessus soit nécessaire ou non. Mais vous devez utiliser les paramètres appropriés.

Le délégué reçoit un MFMailComposeViewController* parameter. Et vous devez utiliser cela au lieu de self lorsque vous quittez le contrôleur. C'est à dire.

Le délégué reçoit le (MFMailComposeViewController *) controller. Et vous devez utiliser cela au lieu de self lorsque vous limitez le MFMailComposeViewController controller. C'est ce que vous voulez rejeter après tout.

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

Une classe d'assistance simple pour gérer le courrier dans Swift. Basé sur la réponse de 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()
        }
    }
}

Placez comme variable d'instance dans AppDelegate-class et appelez-le si nécessaire.

1
Sunkas

Hé, cela est résolu avec iOS 8.3 publié il y a 2 jours.

1
Chriss Mejía