SwiftUI: Send email
Asked Answered
F

13

85

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?

Fontana answered 27/6, 2019 at 5:53 Comment(1)
Probably not an option when you were doing this in 2019, but would the Link view work for this?Porbeagle
F
74

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

Faille answered 27/6, 2019 at 7:8 Comment(11)
wow..it look so different from normal iOS app development with swift. Thanks. it is workingFontana
But if you try twice, it doesn't work. It seems to have a memory leak.Secretary
@FlorentMorin result is non-nil after the first call, hence it won't display again - see self.isShowingMailView && result == nilFaille
OK. I suggest an alternative here, without host mail controller by SwiftUI... gist.github.com/florentmorin/4be7ca70c973c29cbeebbed4e2ef20baSecretary
I updated my answer to include a basic transition, so it looks like the view controller is being presented from the bottom.Faille
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.Vivianviviana
Please see my answer below. I modified the code above to work with presentation environment variable.Analogical
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.Tgroup
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.Astragalus
This is still pretty buggy, the message window randomly turns black. I see similar issues with MFMessageComposeViewControllerUnvalued
The dark mode does not wok properly and the cancel Button is invisible (white-in-white)Elena
A
102

@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()
    }
}
Analogical answered 4/11, 2019 at 12:7 Comment(8)
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?Anthropomorphism
@Anthropomorphism Right after let vc = MFMailComposeViewController() is where you do any of your accustomed configuration.Tedium
.disabled(!MFMailComposeViewController.canSendMail()) isn't working. This was causing crashes on my app when people don't have mail set up. Otherwise, good solution.Devi
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.Carbajal
vc.setSubject("foo") doesn't seem to work. Any ideas?Prudy
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.Tgroup
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.Astragalus
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?Back
F
74

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

Faille answered 27/6, 2019 at 7:8 Comment(11)
wow..it look so different from normal iOS app development with swift. Thanks. it is workingFontana
But if you try twice, it doesn't work. It seems to have a memory leak.Secretary
@FlorentMorin result is non-nil after the first call, hence it won't display again - see self.isShowingMailView && result == nilFaille
OK. I suggest an alternative here, without host mail controller by SwiftUI... gist.github.com/florentmorin/4be7ca70c973c29cbeebbed4e2ef20baSecretary
I updated my answer to include a basic transition, so it looks like the view controller is being presented from the bottom.Faille
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.Vivianviviana
Please see my answer below. I modified the code above to work with presentation environment variable.Analogical
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.Tgroup
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.Astragalus
This is still pretty buggy, the message window randomly turns black. I see similar issues with MFMessageComposeViewControllerUnvalued
The dark mode does not wok properly and the cancel Button is invisible (white-in-white)Elena
D
39

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>) {}
}
Demonstration answered 23/1, 2020 at 10:17 Comment(3)
Nice extra touch of thoroughness!Washhouse
Great! I would like to add the ultimate touch. How to perform the usual sound when the mail is sent ?Jehovist
Great simple and sortedBondie
F
13

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.

Factor answered 16/7, 2020 at 15:25 Comment(3)
Wow this is the best. So simple to use. Worked perfectly. Just remove Utilities.showErrorBanner.Write
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'Astragalus
Also crashes on iPad as popoverPresentationController isn't set, but when I set it, nothing is presented.Multiplicate
F
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(["[email protected]"])
        }
    }
}
Ferromagnetism answered 10/12, 2020 at 5:55 Comment(1)
pulling out composer (MFMailComposeViewController) is a nice move; thanks!Gaffrigged
T
8

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 protected]"])
        }
    }
}
Toaster answered 10/12, 2021 at 19:24 Comment(0)
B
7

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>
Balfore answered 17/7, 2020 at 5:38 Comment(4)
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.Furfuran
Hmm, weird. If you deleted the mail app, it shouldn't have returned true.Balfore
this is a complete copy of https://mcmap.net/q/135755/-how-to-open-mail-app-from-swift. please leave a link to another answer if you refer to itNogging
This is great and all, but I can't seem to find info about adding file attachments for these 3rd party apps.Needs
B
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: "[email protected]") { (isWorked) in
                if !isWorked{ //if mail couldn't be presented
                    // do action
                }
            }
        }, label: {
            Text("Send Email")
        })
Bearcat answered 20/1, 2021 at 13:19 Comment(2)
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!Nematic
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?Noncooperation
E
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: ["[email protected]"],
                 ccRecipients: ["[email protected]"],
                 bccRecipients: ["[email protected]"],
                 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.

Emissary answered 25/6, 2020 at 1:31 Comment(0)
P
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(["[email protected]"])
    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.

Philbo answered 4/9, 2021 at 8:0 Comment(0)
S
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
                }
            )
        }
    }
}
Suicide answered 4/11, 2021 at 22:36 Comment(0)
U
1

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.

Unsnap answered 30/7, 2021 at 13:9 Comment(0)
M
1

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.

Marisolmarissa answered 14/8, 2022 at 14:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.