SwiftUI:发送电子邮件
SwiftUI: Send email
在正常的UIViewController
中Swift,我用这个代码发邮件。[=13=]
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()
}
如何在 SwiftUI 中实现同样的效果?
我需要使用 UIViewControllerRepresentable
吗?
如您所述,您需要通过 UIViewControllerRepresentable
.
将组件移植到 SwiftUI
这是一个简单的实现:
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>) {
}
}
用法:
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)
}
}
}
(在 iPhone 7 Plus 运行 iOS 13 上测试 - 非常有效)
更新为 Xcode11.4
@Matteo 的回答很好但是需要使用presentation 环境变量。我在这里更新了它,它解决了评论中的所有问题。
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>) {
}
}
用法:
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 & Matteo
根据评论,如果您需要在按钮或点击手势上未设置电子邮件时显示警报
@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"))
}
预填充收件人,正文...我还添加了系统声音,与苹果电子邮件发送声音相同
参数:recipients & messageBody 可以在初始化时注入。邮件查看器
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>) {}
}
我已经为它创建了一个 github repository。只需将它添加到您的项目中并像这样使用它:
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()
修饰符检查 MFMailComposeViewController.canSendMail()
是否为 false
,它会自动消除模态并尝试打开 mailto
link.
好吧,我有一个旧代码,我用这种方式在 SwiftUI 中使用。属于这个class的静态函数基本都在我的Utilities.swift文件里。但出于演示目的,我将其移至此处。
同时为了保留委托并正常工作,我将其用作单例模式。
第 1 步:创建电子邮件助手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
}
}
第二步:在SwiftUI中这样调用即可class
Button(action: {
EmailHelper.shared.sendEmail(subject: "Anything...", body: "", to: "")
}) {
Text("Send Email")
}
我在基于 SwiftUI 的项目中使用它。
Yeeee @Hobbes Tige 的回答很好但是...
让我们做得更好!如果用户没有 邮件应用程序 怎么办(就像我没有)。您可以通过尝试其他邮件应用来处理它。
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>
我还改进了@Hobbes 的回答,以便轻松配置主题、收件人等参数。
连要点都懒得看,那SPM呢?
您现在可以在不同的项目中轻松复制粘贴此礼物。
用法;
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"])
}
}
}
我升级并简化了@Mahmud Assan 对新 SwiftUI 生命周期 的回答。
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)
}
}
用法:
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")
})
不幸的是,@Matteo 的解决方案对我来说并不完美。它看起来有问题:(
备选方案
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
)
}
}
用法:
struct ContentView: View {
@State var showEmailComposer = false
var body: some View {
Button("Tap me") {
showEmailComposer = true
}
.mailComposeSheet(isPresented: $showEmailComposer)
}
}
我是 Swift 的新手,如果我做错了什么请告诉我。
在 iOS 14 之前,iOS 上的默认电子邮件应用程序是 Mail。当然,您可以安装其他电子邮件应用程序
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)
}
今天
作为开发人员,我想尊重用户对电子邮件应用程序的选择,无论是 Mail、Edison、Gmail、Outlook 还是 Hey。为此,我无法使用 MFMailComposeViewController。相反,我必须将 mailto 添加到 LSApplicationQueriesSchemes Info.plist 中的键,然后,当用户想要发送电子邮件时,使用此代码:
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [.universalLinksOnly : false]) { (success) in
// Handle success/failure
}
}
与 MFMailComposeViewController 不同,此方法将用户发送到他们选择的电子邮件应用程序,同时关闭源应用程序。不理想。
我认为不需要绑定 isPresented 或结果,因此我建议的解决方案是在调用 MFMailComposeViewControllerDelegate
时使用回调。这也使得结果不可为空。
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
}
}
用法
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
}
)
}
}
}
对于像我这样的人来说,想要一个更好的解决方案而不会使用户的屏幕出现故障,我在这个来自 Medium 的 post 中找到了一个非常好的解决方案。
该解决方案类似于@Mahmud Assan 的回答,但有更多的电子邮件应用程序选项和错误的应用程序警报。
我替换了一些方法代码以允许打开更多电子邮件应用程序,而不仅仅是 Mail 或 gmail。
首先,记得在Info.plist中添加相应的信息,在我的例子中:
<key>LSApplicationQueriesSchemes</key>
<array>
<string>googlegmail</string>
<string>ms-outlook</string>
<string>readdle-spark</string>
<string>ymail</string>
</array>
之后您需要使用以下代码创建一个新的 swift 文件:
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)
}
}
现在,转到您要实现此功能的视图:
struct OpenMailView: View {
var body: some View {
Button("Send email") {
EmailHelper.shared.send(subject: "Help", body: "", to: ["email@gmail.com"])
}
}
}
在正常的UIViewController
中Swift,我用这个代码发邮件。[=13=]
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()
}
如何在 SwiftUI 中实现同样的效果?
我需要使用 UIViewControllerRepresentable
吗?
如您所述,您需要通过 UIViewControllerRepresentable
.
SwiftUI
这是一个简单的实现:
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>) {
}
}
用法:
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)
}
}
}
(在 iPhone 7 Plus 运行 iOS 13 上测试 - 非常有效)
更新为 Xcode11.4
@Matteo 的回答很好但是需要使用presentation 环境变量。我在这里更新了它,它解决了评论中的所有问题。
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>) {
}
}
用法:
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 & Matteo
根据评论,如果您需要在按钮或点击手势上未设置电子邮件时显示警报
@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"))
}
预填充收件人,正文...我还添加了系统声音,与苹果电子邮件发送声音相同
参数:recipients & messageBody 可以在初始化时注入。邮件查看器
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>) {}
}
我已经为它创建了一个 github repository。只需将它添加到您的项目中并像这样使用它:
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()
修饰符检查 MFMailComposeViewController.canSendMail()
是否为 false
,它会自动消除模态并尝试打开 mailto
link.
好吧,我有一个旧代码,我用这种方式在 SwiftUI 中使用。属于这个class的静态函数基本都在我的Utilities.swift文件里。但出于演示目的,我将其移至此处。
同时为了保留委托并正常工作,我将其用作单例模式。
第 1 步:创建电子邮件助手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
}
}
第二步:在SwiftUI中这样调用即可class
Button(action: {
EmailHelper.shared.sendEmail(subject: "Anything...", body: "", to: "")
}) {
Text("Send Email")
}
我在基于 SwiftUI 的项目中使用它。
Yeeee @Hobbes Tige 的回答很好但是...
让我们做得更好!如果用户没有 邮件应用程序 怎么办(就像我没有)。您可以通过尝试其他邮件应用来处理它。
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>
我还改进了@Hobbes 的回答,以便轻松配置主题、收件人等参数。
连要点都懒得看,那SPM呢?
您现在可以在不同的项目中轻松复制粘贴此礼物。
用法;
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"])
}
}
}
我升级并简化了@Mahmud Assan 对新 SwiftUI 生命周期 的回答。
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)
}
}
用法:
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")
})
不幸的是,@Matteo 的解决方案对我来说并不完美。它看起来有问题:(
备选方案
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
)
}
}
用法:
struct ContentView: View {
@State var showEmailComposer = false
var body: some View {
Button("Tap me") {
showEmailComposer = true
}
.mailComposeSheet(isPresented: $showEmailComposer)
}
}
我是 Swift 的新手,如果我做错了什么请告诉我。
在 iOS 14 之前,iOS 上的默认电子邮件应用程序是 Mail。当然,您可以安装其他电子邮件应用程序
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)
}
今天 作为开发人员,我想尊重用户对电子邮件应用程序的选择,无论是 Mail、Edison、Gmail、Outlook 还是 Hey。为此,我无法使用 MFMailComposeViewController。相反,我必须将 mailto 添加到 LSApplicationQueriesSchemes Info.plist 中的键,然后,当用户想要发送电子邮件时,使用此代码:
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [.universalLinksOnly : false]) { (success) in
// Handle success/failure
}
}
与 MFMailComposeViewController 不同,此方法将用户发送到他们选择的电子邮件应用程序,同时关闭源应用程序。不理想。
我认为不需要绑定 isPresented 或结果,因此我建议的解决方案是在调用 MFMailComposeViewControllerDelegate
时使用回调。这也使得结果不可为空。
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
}
}
用法
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
}
)
}
}
}
对于像我这样的人来说,想要一个更好的解决方案而不会使用户的屏幕出现故障,我在这个来自 Medium 的 post 中找到了一个非常好的解决方案。 该解决方案类似于@Mahmud Assan 的回答,但有更多的电子邮件应用程序选项和错误的应用程序警报。
我替换了一些方法代码以允许打开更多电子邮件应用程序,而不仅仅是 Mail 或 gmail。
首先,记得在Info.plist中添加相应的信息,在我的例子中:
<key>LSApplicationQueriesSchemes</key>
<array>
<string>googlegmail</string>
<string>ms-outlook</string>
<string>readdle-spark</string>
<string>ymail</string>
</array>
之后您需要使用以下代码创建一个新的 swift 文件:
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)
}
}
现在,转到您要实现此功能的视图:
struct OpenMailView: View {
var body: some View {
Button("Send email") {
EmailHelper.shared.send(subject: "Help", body: "", to: ["email@gmail.com"])
}
}
}