SwiftUI: отправить электронное письмо

В обычном UIViewController в Swift я использую этот код для отправки почты.

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?


person Khant Thu Linn    schedule 27.06.2019    source источник


Ответы (9)


Как вы упомянули, вам нужно портировать компонент на SwiftUI через UIViewControllerRepresentable.

Вот простая реализация:

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 — работает отлично)

Обновлено для Xcode 11.4

person Matteo Pacini    schedule 27.06.2019
comment
вау... это так отличается от обычной разработки приложений для iOS с помощью Swift. Спасибо. это работает - person Khant Thu Linn; 27.06.2019
comment
Но если вы попробуете дважды, это не сработает. Кажется, есть утечка памяти. - person Florent Morin; 29.06.2019
comment
@FlorentMorin result не равен нулю после первого вызова, поэтому он больше не будет отображаться - см. self.isShowingMailView && result == nil - person Matteo Pacini; 30.06.2019
comment
OK. Я предлагаю альтернативу здесь, без почтового контроллера хоста с помощью SwiftUI... - person Florent Morin; 30.06.2019
comment
Я обновил свой ответ, включив в него базовый переход, поэтому похоже, что контроллер представления представлен снизу. - person Matteo Pacini; 30.06.2019
comment
Любое обновление о том, как представить это в правильном модальном листе? Я до сих пор могу представить его только один раз. Решение ZStack еще более глючное и выглядит довольно плохо, потому что оно не объединяет представление в модальном стеке iOS 13. - person hoshy; 12.09.2019
comment
Пожалуйста, смотрите мой ответ ниже. Я изменил приведенный выше код для работы с переменной среды презентации. - person Hobbes the Tige; 04.11.2019
comment
Если у кого-то есть проблема с тем, что всплывающее окно «Сохранить черновик/Удалить черновик» появляется с задержкой, а клавиатура скрывает задержку, мне помогло добавить .edgesIgnoringSafeArea(.bottom) на лист MailView, чтобы решить эту проблему. - person sp4c38; 27.10.2020
comment
Он работает, но имеет сбой, например, когда данные передаются пользователю, и мы не можем провести пальцем вниз, чтобы закрыть представление, как это делает реализация UIKit. - person Roland Lariotte; 22.12.2020
comment
Это все еще довольно глючно, окно сообщения случайным образом становится черным. Я вижу похожие проблемы с MFMessageComposeViewController. - person bze12; 11.01.2021
comment
Темный режим не работает должным образом, а кнопка отмены невидима (белое в белом) - person iKK; 12.05.2021

Ответ @Matteo хорош, но он должен использовать переменную среды презентации. Я обновил его здесь, и он решает все проблемы в комментариях.

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()
    }
}
person Hobbes the Tige    schedule 04.11.2019
comment
СПАСИБО за то, что связали все вместе так красиво - это работает хорошо! Если кто-то хочет предварительно заполнить адреса To: и CC: вместе со строкой темы, основным текстом и некоторыми вложенными файлами, пожалуйста, где эти параметры находятся в приведенном выше коде? - person ConfusionTowers; 23.12.2019
comment
@ConfusionTowers Сразу после let vc = MFMailComposeViewController() вы выполняете любую привычную конфигурацию. - person Alex Curylo; 27.12.2019
comment
.disabled(!MFMailComposeViewController.canSendMail()) не работает. Это вызывало сбои в моем приложении, когда у людей не была настроена почта. В остальном хорошее решение. - person sfung3; 19.04.2020
comment
Стоит отметить, что я потратил несколько часов, пытаясь заставить это решение работать. (Начиная с SwiftUI 2.0/Xcode 12) принятым ответом было решение, которое работает в моем конкретном случае - с использованием @Binding var isShowing: Bool, а не @Environment(\.presentationMode) var presentation. - person andrewbuilder; 23.08.2020
comment
vc.setSubject("foo") не работает. Есть идеи? - person fankibiber; 19.09.2020
comment
Если у кого-то возникла проблема с тем, что всплывающее окно «Сохранить черновик/Удалить черновик» отображается с задержкой, а клавиатура скрывает задержку, мне помогло добавить .edgesIgnoringSafeArea(.bottom) на лист MailView, чтобы решить эту проблему. - person sp4c38; 27.10.2020
comment
Он работает, но имеет сбой, например, когда данные передаются пользователю, и мы не можем провести пальцем вниз, чтобы закрыть представление, как это делает реализация UIKit. - person Roland Lariotte; 22.12.2020

Ответы правильные 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"))
            }

Чтобы предварительно заполнить To, Body ... также я добавляю системный звук, такой же, как звук отправки электронной почты Apple

Параметры: получатели и 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>) {}
}
person zdravko zdravkin    schedule 23.01.2020
comment
Приятное дополнительное прикосновение тщательности! - person forrest; 10.04.2020
comment
Здорово! Я хотел бы добавить последний штрих. Как сделать обычный звук при отправке почты? - person Nicola Mingotti; 15.07.2020

Ну, у меня есть старый код, который я использовал в SwiftUI таким образом. Статическая функция, принадлежащая этому классу, в основном остается в моем файле Utilities.swift. Но в демонстрационных целях я перенес это сюда.

Кроме того, чтобы сохранить делегат и работать правильно, я использовал его как одноэлементный шаблон.

Шаг 1. Создайте класс помощника по электронной почте

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

Шаг 2. Просто вызовите этот способ в классе SwiftUI

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

Я использую это в своем проекте на основе SwiftUI.

person Mahmud Ahsan    schedule 16.07.2020
comment
Вау это лучшее. Так просто в использовании. Работал отлично. Просто удалите Utilities.showErrorBanner. - person thegathering; 28.10.2020
comment
К сожалению, это решение приводит к сбою приложения при попытке открыть почтовое приложение дважды подряд. Эта ошибка отображается «[PPT] Ошибка при создании CFMessagePort, необходимого для связи с PPT». - person Roland Lariotte; 22.12.2020

Я также улучшил ответ @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(["[email protected]"])
        }
    }
}
person Enes Karaosman    schedule 10.12.2020

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

создать URL-адрес электронной почты

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
    }

Информация.plist

<key>LSApplicationQueriesSchemes</key>
<array>
    <string>googlegmail</string>
    <string>ms-outlook</string>
    <string>readdle-spark</string>
    <string>ymail</string>
</array>
person Stephen Lee    schedule 17.07.2020
comment
Хорошее дополнение! Вы правы, не все используют приложение Apple Mail! Что интересно, у меня не установлено приложение Apple Mail, но MFMailComposeViewController.canSendMail() по-прежнему возвращает значение true. - person Dom; 08.09.2020
comment
Хм, странно. Если вы удалили почтовое приложение, оно не должно было вернуть значение true. - person Stephen Lee; 09.09.2020
comment
это полная копия stackoverflow.com/a/55765362/6898849. пожалуйста, оставьте ссылку на другой ответ, если вы ссылаетесь на него - person ramzesenok; 14.09.2020

Я обновил и упростил ответ @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: "[email protected]") { (isWorked) in
                if !isWorked{ //if mail couldn't be presented
                    // do action
                }
            }
        }, label: {
            Text("Send Email")
        })
person batuhankrbb    schedule 20.01.2021

Я создал для него репозиторий github. просто добавьте его в свой проект и используйте так:

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() проверяет, является ли MFMailComposeViewController.canSendMail() false, он автоматически закрывает модальное окно и пытается открыть ссылку mailto.

person Mohammad Rahchamani    schedule 25.06.2020

К сожалению, решение @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, скажите, пожалуйста, если я делаю что-то не так.

person Denwakeup    schedule 30.07.2021