SwiftUI: Send email
@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 SwiftUIimport UIKitimport MessageUIstruct 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 SwiftUIimport MessageUIstruct 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() }}
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
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>? = nilHStack { 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 AVFoundationimport Foundationimport MessageUIimport SwiftUIimport UIKitstruct 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>) {}}