77

In a normal UIViewController in Swift, I use this code to send a mail.

let mailComposeViewController = configuredMailComposeViewController()

mailComposeViewController.navigationItem.leftBarButtonItem?.style = .plain
mailComposeViewController.navigationItem.rightBarButtonItem?.style = .plain
mailComposeViewController.navigationBar.tintColor = UIColor.white

if MFMailComposeViewController.canSendMail() {
    self.present(mailComposeViewController, animated: true, completion: nil)
} else {
    self.showSendMailErrorAlert()
}

How can I achieve the same in SwiftUI?

Do I need to use UIViewControllerRepresentable?

Matteo Pacini
  • 21,796
  • 7
  • 67
  • 74
Khant Thu Linn
  • 5,905
  • 7
  • 52
  • 120
  • Probably not an option when you were doing this in 2019, but would the Link view work for this? – spig Feb 12 '22 at 00:48

13 Answers13

96

@Matteo's answer is good but it needs to use the presentation environment variable. I have updated it here and it addresses all of the concerns in the comments.

import SwiftUI
import UIKit
import MessageUI

struct MailView: UIViewControllerRepresentable {

    @Environment(\.presentationMode) var presentation
    @Binding var result: Result<MFMailComposeResult, Error>?

    class Coordinator: NSObject, MFMailComposeViewControllerDelegate {

        @Binding var presentation: PresentationMode
        @Binding var result: Result<MFMailComposeResult, Error>?

        init(presentation: Binding<PresentationMode>,
             result: Binding<Result<MFMailComposeResult, Error>?>) {
            _presentation = presentation
            _result = result
        }

        func mailComposeController(_ controller: MFMailComposeViewController,
                                   didFinishWith result: MFMailComposeResult,
                                   error: Error?) {
            defer {
                $presentation.wrappedValue.dismiss()
            }
            guard error == nil else {
                self.result = .failure(error!)
                return
            }
            self.result = .success(result)
        }
    }

    func makeCoordinator() -> Coordinator {
        return Coordinator(presentation: presentation,
                           result: $result)
    }

    func makeUIViewController(context: UIViewControllerRepresentableContext<MailView>) -> MFMailComposeViewController {
        let vc = MFMailComposeViewController()
        vc.mailComposeDelegate = context.coordinator
        return vc
    }

    func updateUIViewController(_ uiViewController: MFMailComposeViewController,
                                context: UIViewControllerRepresentableContext<MailView>) {

    }
}

Usage:

import SwiftUI
import MessageUI

struct ContentView: View {

   @State var result: Result<MFMailComposeResult, Error>? = nil
   @State var isShowingMailView = false

    var body: some View {
        Button(action: {
            self.isShowingMailView.toggle()
        }) {
            Text("Tap Me")
        }
        .disabled(!MFMailComposeViewController.canSendMail())
        .sheet(isPresented: $isShowingMailView) {
            MailView(result: self.$result)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
Hobbes the Tige
  • 3,753
  • 2
  • 22
  • 21
  • 3
    THANK YOU for tying everything together so nicely -- it works well! If one wants to pre-populate a To: and CC: address, along with a subject line, body text, and some attached files, where do those parameters go in the code above, please? – ConfusionTowers Dec 22 '19 at 23:32
  • 1
    @ConfusionTowers Right after `let vc = MFMailComposeViewController()` is where you do any of your accustomed configuration. – Alex Curylo Dec 26 '19 at 21:46
  • `.disabled(!MFMailComposeViewController.canSendMail())` isn't working. This was causing crashes on my app when people don't have mail set up. Otherwise, good solution. – sfung3 Apr 19 '20 at 02:36
  • 1
    Worth a comment that I spent a few hours attempting to make this solution work. (As of SwiftUI 2.0 / Xcode 12) the accepted answer was the solution that works in my particular case - using `@Binding var isShowing: Bool` and not `@Environment(\.presentationMode) var presentation`. – andrewbuilder Aug 23 '20 at 15:50
  • `vc.setSubject("foo")` doesn't seem to work. Any ideas? – fankibiber Sep 18 '20 at 22:18
  • 1
    If anyone has the problem that the Save draft/Delete draft popup appears laggy and the keyboard hides laggy it helped for me to add .edgesIgnoringSafeArea(.bottom) to the MailView sheet to resolve this problem. – sp4c38 Oct 27 '20 at 14:01
  • It works but does have a glitch as when data are populated to the user and we can not swipe down to dismiss the view like the UIKit implementation does. – Roland Lariotte Dec 22 '20 at 13:46
  • Unfortunately, after the view appear animation is completed, font jumps from one size to another, like view is updated to another preferences after animation is done. I cannot also close the view with swipe action (only cancel). Though the same MFMailComposeViewController presented with UIKit works perfectly. Anybody knows the reason and solution? – bodich Jan 07 '22 at 18:16
71

As you mentioned, you need to port the component to SwiftUI via UIViewControllerRepresentable.

Here's a simple implementation:

struct MailView: UIViewControllerRepresentable {

    @Binding var isShowing: Bool
    @Binding var result: Result<MFMailComposeResult, Error>?

    class Coordinator: NSObject, MFMailComposeViewControllerDelegate {

        @Binding var isShowing: Bool
        @Binding var result: Result<MFMailComposeResult, Error>?

        init(isShowing: Binding<Bool>,
             result: Binding<Result<MFMailComposeResult, Error>?>) {
            _isShowing = isShowing
            _result = result
        }

        func mailComposeController(_ controller: MFMailComposeViewController,
                                   didFinishWith result: MFMailComposeResult,
                                   error: Error?) {
            defer {
                isShowing = false
            }
            guard error == nil else {
                self.result = .failure(error!)
                return
            }
            self.result = .success(result)
        }
    }

    func makeCoordinator() -> Coordinator {
        return Coordinator(isShowing: $isShowing,
                           result: $result)
    }

    func makeUIViewController(context: UIViewControllerRepresentableContext<MailView>) -> MFMailComposeViewController {
        let vc = MFMailComposeViewController()
        vc.mailComposeDelegate = context.coordinator
        return vc
    }

    func updateUIViewController(_ uiViewController: MFMailComposeViewController,
                                context: UIViewControllerRepresentableContext<MailView>) {

    }
}

Usage:

struct ContentView: View {

    @State var result: Result<MFMailComposeResult, Error>? = nil
    @State var isShowingMailView = false

    var body: some View {

        VStack {
            if MFMailComposeViewController.canSendMail() {
                Button("Show mail view") {
                    self.isShowingMailView.toggle()
                }
            } else {
                Text("Can't send emails from this device")
            }
            if result != nil {
                Text("Result: \(String(describing: result))")
                    .lineLimit(nil)
            }
        }
        .sheet(isPresented: $isShowingMailView) {
            MailView(isShowing: self.$isShowingMailView, result: self.$result)
        }

    }

}

(Tested on iPhone 7 Plus running iOS 13 - works like a charm)

Updated for Xcode 11.4

Ralf Ebert
  • 3,556
  • 3
  • 29
  • 43
Matteo Pacini
  • 21,796
  • 7
  • 67
  • 74
  • 1
    wow..it look so different from normal iOS app development with swift. Thanks. it is working – Khant Thu Linn Jun 27 '19 at 09:02
  • But if you try twice, it doesn't work. It seems to have a memory leak. – Florent Morin Jun 29 '19 at 20:58
  • @FlorentMorin `result` is non-nil after the first call, hence it won't display again - see `self.isShowingMailView && result == nil` – Matteo Pacini Jun 29 '19 at 21:10
  • 1
    OK. I suggest an alternative here, without host mail controller by SwiftUI... https://gist.github.com/florentmorin/4be7ca70c973c29cbeebbed4e2ef20ba – Florent Morin Jun 29 '19 at 22:02
  • I updated my answer to include a basic transition, so it looks like the view controller is being presented from the bottom. – Matteo Pacini Jun 30 '19 at 04:45
  • Any update on how to present this in a proper modal aka sheet? I still can only present it once. The ZStack solution is even glitchier and looks pretty bad, because it doesn't stack the view in the iOS 13 modal stack. – hoshy Sep 12 '19 at 16:46
  • Please see my answer below. I modified the code above to work with presentation environment variable. – Hobbes the Tige Nov 04 '19 at 12:08
  • 1
    If anyone has the problem that the Save draft/Delete draft popup appears laggy and the keyboard hides laggy it helped for me to add `.edgesIgnoringSafeArea(.bottom)` to the MailView sheet to resolve this problem. – sp4c38 Oct 27 '20 at 14:00
  • It works but does have a glitch as when data are populated to the user and we can not swipe down to dismiss the view like the UIKit implementation does. – Roland Lariotte Dec 22 '20 at 13:47
  • This is still pretty buggy, the message window randomly turns black. I see similar issues with MFMessageComposeViewController – bze12 Jan 11 '21 at 02:55
  • The dark mode does not wok properly and the cancel Button is invisible (white-in-white) – iKK May 12 '21 at 07:36
36

Answers are correct Hobbes the Tige & Matteo

From the comments, if you need to show an alert if no email is set up on the button or tap gesture

@State var isShowingMailView = false
@State var alertNoMail = false
@State var result: Result<MFMailComposeResult, Error>? = nil

HStack {
                Image(systemName: "envelope.circle").imageScale(.large)
                Text("Contact")
            }.onTapGesture {
                MFMailComposeViewController.canSendMail() ? self.isShowingMailView.toggle() : self.alertNoMail.toggle()
            }
                //            .disabled(!MFMailComposeViewController.canSendMail())
                .sheet(isPresented: $isShowingMailView) {
                    MailView(result: self.$result)
            }
            .alert(isPresented: self.$alertNoMail) {
                Alert(title: Text("NO MAIL SETUP"))
            }

To pre-populate To, Body ... also I add system sound same as Apple email sending sound

Parameters: recipients & messageBody can be injected when you init. MailView

import AVFoundation
import Foundation
import MessageUI
import SwiftUI
import UIKit

struct MailView: UIViewControllerRepresentable {
    @Environment(\.presentationMode) var presentation
    @Binding var result: Result<MFMailComposeResult, Error>?
    var recipients = [String]()
    var messageBody = ""

    class Coordinator: NSObject, MFMailComposeViewControllerDelegate {
        @Binding var presentation: PresentationMode
        @Binding var result: Result<MFMailComposeResult, Error>?

        init(presentation: Binding<PresentationMode>,
             result: Binding<Result<MFMailComposeResult, Error>?>)
        {
            _presentation = presentation
            _result = result
        }

        func mailComposeController(_: MFMailComposeViewController,
                                   didFinishWith result: MFMailComposeResult,
                                   error: Error?)
        {
            defer {
                $presentation.wrappedValue.dismiss()
            }
            guard error == nil else {
                self.result = .failure(error!)
                return
            }
            self.result = .success(result)
            
            if result == .sent {
            AudioServicesPlayAlertSound(SystemSoundID(1001))
            }
        }
    }

    func makeCoordinator() -> Coordinator {
        return Coordinator(presentation: presentation,
                           result: $result)
    }

    func makeUIViewController(context: UIViewControllerRepresentableContext<MailView>) -> MFMailComposeViewController {
        let vc = MFMailComposeViewController()
        vc.setToRecipients(recipients)
        vc.setMessageBody(messageBody, isHTML: true)
        vc.mailComposeDelegate = context.coordinator
        return vc
    }

    func updateUIViewController(_: MFMailComposeViewController,
                                context _: UIViewControllerRepresentableContext<MailView>) {}
}
zdravko zdravkin
  • 2,090
  • 19
  • 21
11

I also improved @Hobbes answer to easily configure parameters like, subject, recipients.

Checkout this gist

Even too lazy to checkout gist, then what about a SPM?

You can now easily copy paste this gift across different projects.

Usage;

import SwiftUI
import MessagesUI
// import SwiftUIEKtensions // via SPM

@State private var result: Result<MFMailComposeResult, Error>? = nil
@State private var isShowingMailView = false

var body: some View {
    Form {
        Button(action: {
            if MFMailComposeViewController.canSendMail() {
                self.isShowingMailView.toggle()
            } else {
                print("Can't send emails from this device")
            }
            if result != nil {
                print("Result: \(String(describing: result))")
            }
        }) {
            HStack {
                Image(systemName: "envelope")
                Text("Contact Us")
            }
        }
        // .disabled(!MFMailComposeViewController.canSendMail())
    }
    .sheet(isPresented: $isShowingMailView) {
        MailView(result: $result) { composer in
            composer.setSubject("Secret")
            composer.setToRecipients(["fancy@mail.com"])
        }
    }
}
Enes Karaosman
  • 1,889
  • 20
  • 27
10

Well, I have an old code that I used in SwiftUI in this way. The static function belongs to this class basically stays in my Utilities.swift file. But for demonstration purposes, I moved that in here.

Also to retain the delegate and works correctly, I have used this one as a singleton pattern.

Step 1: Create an Email Helper class

import Foundation
import MessageUI

class EmailHelper: NSObject, MFMailComposeViewControllerDelegate {
    public static let shared = EmailHelper()
    private override init() {
        //
    }
    
    func sendEmail(subject:String, body:String, to:String){
        if !MFMailComposeViewController.canSendMail() {
            // Utilities.showErrorBanner(title: "No mail account found", subtitle: "Please setup a mail account")
            return //EXIT
        }
        
        let picker = MFMailComposeViewController()
        
        picker.setSubject(subject)
        picker.setMessageBody(body, isHTML: true)
        picker.setToRecipients([to])
        picker.mailComposeDelegate = self
        
        EmailHelper.getRootViewController()?.present(picker, animated: true, completion: nil)
    }
    
    func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
        EmailHelper.getRootViewController()?.dismiss(animated: true, completion: nil)
    }
    
    static func getRootViewController() -> UIViewController? {
        (UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate)?.window?.rootViewController

         // OR If you use SwiftUI 2.0 based WindowGroup try this one
         // UIApplication.shared.windows.first?.rootViewController
    }
}

Step 2: Just call this way in SwiftUI class

Button(action: {
   EmailHelper.shared.sendEmail(subject: "Anything...", body: "", to: "")
 }) {
     Text("Send Email")
 }

I am using this is in my SwiftUI based project.

Mahmud Ahsan
  • 1,755
  • 19
  • 18
  • Wow this is the best. So simple to use. Worked perfectly. Just remove Utilities.showErrorBanner. – thegathering Oct 28 '20 at 12:18
  • Unfortunately, this solution crashes the app when you attempt to open the mail app twice in a row. This error is shown ' [PPT] Error creating the CFMessagePort needed to communicate with PPT' – Roland Lariotte Dec 21 '20 at 22:33
  • Also crashes on iPad as `popoverPresentationController` isn't set, but when I set it, nothing is presented. – Darren Nov 29 '22 at 14:57
8

Yeeee @Hobbes the Tige answer is good but...

Let's make it even better! What if user doesn't have Mail app (like I don't). You can handle it by trying out other mail apps.

if MFMailComposeViewController.canSendMail() {
   self.showMailView.toggle()
} else if let emailUrl = Utils.createEmailUrl(subject: "Yo, sup?", body: "hot dog") {
   UIApplication.shared.open(emailUrl)
} else {
   self.alertNoMail.toggle()
}

createEmailUrl

static func createEmailUrl(subject: String, body: String) -> URL? {
        let to = YOUR_EMAIL
        let subjectEncoded = subject.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)!
        let bodyEncoded = body.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)!

        let gmailUrl = URL(string: "googlegmail://co?to=\(to)&subject=\(subjectEncoded)&body=\(bodyEncoded)")
        let outlookUrl = URL(string: "ms-outlook://compose?to=\(to)&subject=\(subjectEncoded)")
        let yahooMail = URL(string: "ymail://mail/compose?to=\(to)&subject=\(subjectEncoded)&body=\(bodyEncoded)")
        let sparkUrl = URL(string: "readdle-spark://compose?recipient=\(to)&subject=\(subjectEncoded)&body=\(bodyEncoded)")
        let defaultUrl = URL(string: "mailto:\(to)?subject=\(subjectEncoded)&body=\(bodyEncoded)")

        if let gmailUrl = gmailUrl, UIApplication.shared.canOpenURL(gmailUrl) {
            return gmailUrl
        } else if let outlookUrl = outlookUrl, UIApplication.shared.canOpenURL(outlookUrl) {
            return outlookUrl
        } else if let yahooMail = yahooMail, UIApplication.shared.canOpenURL(yahooMail) {
            return yahooMail
        } else if let sparkUrl = sparkUrl, UIApplication.shared.canOpenURL(sparkUrl) {
            return sparkUrl
        }

        return defaultUrl
    }

Info.plist

<key>LSApplicationQueriesSchemes</key>
<array>
    <string>googlegmail</string>
    <string>ms-outlook</string>
    <string>readdle-spark</string>
    <string>ymail</string>
</array>
Stephen Lee
  • 309
  • 3
  • 10
  • Nice addition! You are correct not everyone uses the Apple Mail app! What's interesting is that I do not have the Apple Mail app installed yet the MFMailComposeViewController.canSendMail() still returns true. – Dom Sep 08 '20 at 19:14
  • Hmm, weird. If you deleted the mail app, it shouldn't have returned true. – Stephen Lee Sep 08 '20 at 21:55
  • 6
    this is a complete copy of https://stackoverflow.com/a/55765362/6898849. please leave a link to another answer if you refer to it – ramzesenok Sep 14 '20 at 20:49
  • 1
    This is great and all, but I can't seem to find info about adding file attachments for these 3rd party apps. – TealShift Dec 15 '21 at 21:50
6

I upgraded and simplified @Mahmud Assan's answer for the new SwiftUI Lifecycle.

import Foundation
import MessageUI

class EmailService: NSObject, MFMailComposeViewControllerDelegate {
public static let shared = EmailService()

func sendEmail(subject:String, body:String, to:String, completion: @escaping (Bool) -> Void){
 if MFMailComposeViewController.canSendMail(){
    let picker = MFMailComposeViewController()
    picker.setSubject(subject)
    picker.setMessageBody(body, isHTML: true)
    picker.setToRecipients([to])
    picker.mailComposeDelegate = self
    
   UIApplication.shared.windows.first?.rootViewController?.present(picker,  animated: true, completion: nil)
}
  completion(MFMailComposeViewController.canSendMail())
}

func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
    controller.dismiss(animated: true, completion: nil)
     }
}

Usage:

Button(action: {
            EmailService.shared.sendEmail(subject: "hello", body: "this is body", to: "asd@gmail.com") { (isWorked) in
                if !isWorked{ //if mail couldn't be presented
                    // do action
                }
            }
        }, label: {
            Text("Send Email")
        })
batuhankrbb
  • 598
  • 7
  • 10
  • this is great. Care to add an explanation on how you figured this one out? I think it'd help. If not, thanks any way! – Tank12 Mar 01 '23 at 05:02
  • You now have the following warning for `UIApplication.shared.windows`: 'windows' was deprecated in iOS 15.0: Use UIWindowScene.windows on a relevant window scene instead. Do you have a solution? – lorenzo Mar 14 '23 at 14:51
5

For anyone like me, wanting a better solution without glitching the screen of the user, i founded a very nice solution in this post from Medium. The solution is similar to @Mahmud Assan's answer, but with more email app options and app alert with error.

I replaced some code for a method to allow the opening of more email apps, not only Mail or gmail.

First, remember to add the respective info in Info.plist, in my case:

<key>LSApplicationQueriesSchemes</key>
<array>
    <string>googlegmail</string>
    <string>ms-outlook</string>
    <string>readdle-spark</string>
    <string>ymail</string>
</array>

After that you need to create a new swift file with the following code:

import SwiftUI
import MessageUI

class EmailHelper: NSObject {
    /// singleton
    static let shared = EmailHelper()
    private override init() {}
}

extension EmailHelper {
    
    func send(subject: String, body: String, to: [String]) {
        
        let scenes = UIApplication.shared.connectedScenes
        let windowScene = scenes.first as? UIWindowScene
        
        guard let viewController = windowScene?.windows.first?.rootViewController else {
            return
        }
        
        if !MFMailComposeViewController.canSendMail() {
            let subjectEncoded = subject.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
            let bodyEncoded = body.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
            let mails = to.joined(separator: ",")
            
            let alert = UIAlertController(title: "Cannot open Mail!", message: "", preferredStyle: .actionSheet)
            
            var haveExternalMailbox = false
            
            if let url = createEmailUrl(to: mails, subject: subjectEncoded, body: bodyEncoded), UIApplication.shared.canOpenURL(url) {
                haveExternalMailbox = true
                alert.addAction(UIAlertAction(title: "Gmail", style: .default, handler: { (action) in
                    UIApplication.shared.open(url)
                }))
            }
            
            if haveExternalMailbox {
                alert.message = "Would you like to open an external mailbox?"
            } else {
                alert.message = "Please add your mail to Settings before using the mail service."
                
                if let settingsUrl = URL(string: UIApplication.openSettingsURLString),
                   UIApplication.shared.canOpenURL(settingsUrl) {
                    alert.addAction(UIAlertAction(title: "Open Settings App", style: .default, handler: { (action) in
                        UIApplication.shared.open(settingsUrl)
                    }))
                }
            }
            
            alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
            viewController.present(alert, animated: true, completion: nil)
            return
        }
        
        let mailCompose = MFMailComposeViewController()
        mailCompose.setSubject(subject)
        mailCompose.setMessageBody(body, isHTML: false)
        mailCompose.setToRecipients(to)
        mailCompose.mailComposeDelegate = self
        
        viewController.present(mailCompose, animated: true, completion: nil)
    }
    
    private func createEmailUrl(to: String, subject: String, body: String) -> URL? {
        let subjectEncoded = subject.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
        let bodyEncoded = body.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
        
        let gmailUrl = URL(string: "googlegmail://co?to=\(to)&subject=\(subjectEncoded)&body=\(bodyEncoded)")
        let outlookUrl = URL(string: "ms-outlook://compose?to=\(to)&subject=\(subjectEncoded)")
        let yahooMail = URL(string: "ymail://mail/compose?to=\(to)&subject=\(subjectEncoded)&body=\(bodyEncoded)")
        let sparkUrl = URL(string: "readdle-spark://compose?recipient=\(to)&subject=\(subjectEncoded)&body=\(bodyEncoded)")
        let defaultUrl = URL(string: "mailto:\(to)?subject=\(subjectEncoded)&body=\(bodyEncoded)")
        
        if let gmailUrl = gmailUrl, UIApplication.shared.canOpenURL(gmailUrl) {
            return gmailUrl
        } else if let outlookUrl = outlookUrl, UIApplication.shared.canOpenURL(outlookUrl) {
            return outlookUrl
        } else if let yahooMail = yahooMail, UIApplication.shared.canOpenURL(yahooMail) {
            return yahooMail
        } else if let sparkUrl = sparkUrl, UIApplication.shared.canOpenURL(sparkUrl) {
            return sparkUrl
        }
        
        return defaultUrl
    }
    
}

// MARK: - MFMailComposeViewControllerDelegate
extension EmailHelper: MFMailComposeViewControllerDelegate {
    
    func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
        controller.dismiss(animated: true, completion: nil)
    }
}

Now, go to the view where you want to implement this:

  struct OpenMailView: View {
    var body: some View {
        Button("Send email") {
            EmailHelper.shared.send(subject: "Help", body: "", to: ["email@gmail.com"])
        }
    }
}
Alessandro Pace
  • 206
  • 4
  • 8
2

I've created a github repository for it. just add it to your project and use it like this:

struct ContentView: View {

@State var showMailSheet = false

var body: some View {
    NavigationView {
        Button(action: {
            self.showMailSheet.toggle()
        }) {
            Text("compose")
        }
    }
    .sheet(isPresented: self.$showMailSheet) {
        MailView(isShowing: self.$showMailSheet,
                 resultHandler: {
                    value in
                    switch value {
                    case .success(let result):
                        switch result {
                        case .cancelled:
                            print("cancelled")
                        case .failed:
                            print("failed")
                        case .saved:
                            print("saved")
                        default:
                            print("sent")
                        }
                    case .failure(let error):
                        print("error: \(error.localizedDescription)")
                    }
        },
                 subject: "test Subjet",
                 toRecipients: ["recipient@test.com"],
                 ccRecipients: ["cc@test.com"],
                 bccRecipients: ["bcc@test.com"],
                 messageBody: "works like a charm!",
                 isHtml: false)
        .safe()
        
    }

  }
}

safe() modifier checks if MFMailComposeViewController.canSendMail() is false, it automatically dismesses the modal and tries to open a mailto link.

Mohammad Rahchamani
  • 5,002
  • 1
  • 26
  • 36
2

Before iOS 14, the default email app on iOS was Mail. Of course, you could have had other email apps installed

   if MFMailComposeViewController.canSendMail() {
    let mailController = MFMailComposeViewController(rootViewController: self)
    mailController.setSubject("Test")
    mailController.setToRecipients(["mail@test.com"])
    mailController.mailComposeDelegate = self
    present(mailController, animated: true, completion: nil)
}

Today As a developer, I want to respect the user’s choice of email app, whether it’s Mail, Edison, Gmail, Outlook, or Hey. To do that, I can’t use MFMailComposeViewController. Instead, I have to add mailto to the LSApplicationQueriesSchemes key in Info.plist and then, when the user wants to send an email, use this code:

if UIApplication.shared.canOpenURL(url) {
    UIApplication.shared.open(url, options: [.universalLinksOnly : false]) { (success) in
        // Handle success/failure
    }
}

Unlike MFMailComposeViewController, this approach sends the user to their choice of email app and, at the same time, closes the source app. It’s not ideal.

Chaitanya
  • 52
  • 4
2

I don't see the need of binding the isPresented or the result so my proposed solution is to use a callback when the MFMailComposeViewControllerDelegate is called. This also makes the result not nullable.

import Foundation
import MessageUI
import SwiftUI
import UIKit

public struct MailView: UIViewControllerRepresentable {
    public struct Attachment {
        public let data: Data
        public let mimeType: String
        public let filename: String

        public init(data: Data, mimeType: String, filename: String) {
            self.data = data
            self.mimeType = mimeType
            self.filename = filename
        }
    }

    public let onResult: ((Result<MFMailComposeResult, Error>) -> Void)

    public let subject: String?
    public let message: String?
    public let attachment: Attachment?

    public class Coordinator: NSObject, MFMailComposeViewControllerDelegate {
        public var onResult: ((Result<MFMailComposeResult, Error>) -> Void)

        init(onResult: @escaping ((Result<MFMailComposeResult, Error>) -> Void)) {
            self.onResult = onResult
        }

        public func mailComposeController(
            _ controller: MFMailComposeViewController,
            didFinishWith result: MFMailComposeResult,
            error: Error?
        ) {
            if let error = error {
                self.onResult(.failure(error))
            } else {
                self.onResult(.success(result))
            }
        }
    }

    public init(
        subject: String? = nil,
        message: String? = nil,
        attachment: MailView.Attachment? = nil,
        onResult: @escaping ((Result<MFMailComposeResult, Error>) -> Void)
    ) {
        self.subject = subject
        self.message = message
        self.attachment = attachment
        self.onResult = onResult
    }

    public func makeCoordinator() -> Coordinator {
        Coordinator(onResult: onResult)
    }

    public func makeUIViewController(
        context: UIViewControllerRepresentableContext<MailView>
    ) -> MFMailComposeViewController {
        let controller = MFMailComposeViewController()
        controller.mailComposeDelegate = context.coordinator
        if let subject = subject {
            controller.setSubject(subject)
        }
        if let message = message {
            controller.setMessageBody(message, isHTML: false)
        }
        if let attachment = attachment {
            controller.addAttachmentData(
                attachment.data,
                mimeType: attachment.mimeType,
                fileName: attachment.filename
            )
        }
        return controller
    }

    public func updateUIViewController(
        _ uiViewController: MFMailComposeViewController,
        context: UIViewControllerRepresentableContext<MailView>
    ) {
        // nothing to do here
    }
}

Usage

struct ContentView: View {
    @State var showEmailComposer = false

    var body: some View {
        Button("Tap me") {
            showEmailComposer = true
        }
        .sheet(isPresented: $showEmailComposer) {
            MailView(
                subject: "Email subject",
                message: "Message",
                attachment: nil,
                onResult: { _ in
                     // Handle the result if needed.
                     self.showEmailComposer = false
                }
            )
        }
    }
}
Mihai Georgescu
  • 674
  • 8
  • 20
0

Unfortunately, @Matteo's solution doesn't work perfectly for me. It looks buggy :(

Alternative solution

struct MailComposeSheet<T: View>: UIViewControllerRepresentable {
    let view: T

    @Binding var isPresented: Bool

    func makeUIViewController(context: Context) -> UIHostingController<T> {
        UIHostingController(rootView: view)
    }

    func updateUIViewController(_ uiViewController: UIHostingController<T>, context: Context) {
        uiViewController.rootView = view

        if isPresented, uiViewController.presentedViewController == nil {
            let picker = MFMailComposeViewController()

            picker.mailComposeDelegate = context.coordinator
            picker.presentationController?.delegate = context.coordinator

            uiViewController.present(picker, animated: true)
        }
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    class Coordinator: NSObject, MFMailComposeViewControllerDelegate, UIAdaptivePresentationControllerDelegate {
        var parent: MailComposeSheet

        init(_ mailComposeSheet: MailComposeSheet) {
            self.parent = mailComposeSheet
        }

        func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
            controller.dismiss(animated: true) { [weak self] in
                self?.parent.isPresented = false
            }
        }

        func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
            parent.isPresented = false
        }
    }
}

extension View {
    func mailComposeSheet(isPresented: Binding<Bool>) -> some View {
        MailComposeSheet(
            view: self,
            isPresented: isPresented
        )
    }
}

Usage:

struct ContentView: View {
    @State var showEmailComposer = false

    var body: some View {
        Button("Tap me") {
            showEmailComposer = true
        }
        .mailComposeSheet(isPresented: $showEmailComposer)
    }
}

I'm new to Swift, please tell me if I'm doing something wrong.

Denwakeup
  • 256
  • 2
  • 4
0

I went through all the answers above - continued to get AXSERVER / CPT port errors.

What worked for me

     Button(action: {
                            let email = "mailto://"
                            let emailformatted = email + centreStaff.userName // from MongoDB Atlas
                            guard let url = URL(string: emailformatted) else { return }
                            UIApplication.shared.open(url)
                              }) {
                             Image (systemName: "envelope.circle.fill")
                                .symbolRenderingMode(.multicolor)
                         }

opens Outlook with name of the staff filled in...and Boom! email sent.