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?

Вероятно, это был не вариант, когда вы делали это в 2019 году, но подойдет ли для этого представление «Ссылка»?

spig 12.02.2022 01:48
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
63
1
19 715
12
Перейти к ответу Данный вопрос помечен как решенный

Ответы 12

Ответ принят как подходящий

Как вы упомянули, вам нужно портировать компонент на 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

вау... это так отличается от обычной разработки приложений для iOS с помощью Swift. Спасибо. это работает

Khant Thu Linn 27.06.2019 11:02

Но если вы попробуете дважды, это не сработает. Кажется, есть утечка памяти.

Florent Morin 29.06.2019 22:58

@FlorentMorin result не равен нулю после первого вызова, поэтому он больше не будет отображаться - см. self.isShowingMailView && result == nil

Matteo Pacini 29.06.2019 23:10

OK. Я предлагаю альтернативу здесь, без почтового контроллера хоста SwiftUI... gist.github.com/florentmorin/4be7ca70c973c29cbeebbed4e2ef20b‌​а

Florent Morin 30.06.2019 00:02

Я обновил свой ответ, включив в него базовый переход, поэтому похоже, что контроллер представления представлен снизу.

Matteo Pacini 30.06.2019 06:45

Любое обновление о том, как представить это в правильном модальном листе? Я до сих пор могу представить его только один раз. Решение ZStack еще более глючное и выглядит довольно плохо, потому что оно не объединяет представление в модальном стеке iOS 13.

hoshy 12.09.2019 18:46

Пожалуйста, смотрите мой ответ ниже. Я изменил приведенный выше код для работы с переменной среды презентации.

Hobbes the Tige 04.11.2019 13:08

Если у кого-то возникла проблема с тем, что всплывающее окно «Сохранить черновик/Удалить черновик» работает с задержкой, а клавиатура скрывает задержку, мне помогло добавить .edgesIgnoringSafeArea(.bottom) на лист MailView, чтобы решить эту проблему.

sp4c38 27.10.2020 15:00

Он работает, но имеет сбой, например, когда данные передаются пользователю, и мы не можем провести пальцем вниз, чтобы закрыть представление, как это делает реализация UIKit.

Roland Lariotte 22.12.2020 14:47

Это все еще довольно глючно, окно сообщения случайным образом становится черным. Я вижу похожие проблемы с MFMessageComposeViewController.

bze12 11.01.2021 03:55

Темный режим не работает должным образом, а кнопка отмены невидима (белое в белом)

iKK 12.05.2021 09:36

Ответ @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()
    }
}

СПАСИБО за то, что связали все вместе так красиво - это работает хорошо! Если кто-то хочет предварительно заполнить адреса To: и CC: вместе со строкой темы, основным текстом и некоторыми вложенными файлами, пожалуйста, где эти параметры находятся в приведенном выше коде?

ConfusionTowers 23.12.2019 00:32

@ConfusionTowers Сразу после let vc = MFMailComposeViewController() вы выполняете любую привычную конфигурацию.

Alex Curylo 26.12.2019 22:46
.disabled(!MFMailComposeViewController.canSendMail()) не работает. Это вызывало сбои в моем приложении, когда у людей не была настроена почта. В остальном хорошее решение.
sfung3 19.04.2020 04:36

Стоит отметить, что я потратил несколько часов, пытаясь заставить это решение работать. (Начиная с SwiftUI 2.0/Xcode 12) принятым ответом было решение, которое работает в моем конкретном случае - с использованием @Binding var isShowing: Bool, а не @Environment(\.presentationMode) var presentation.

andrewbuilder 23.08.2020 17:50
vc.setSubject("foo") не работает. Есть идеи?
fankibiber 19.09.2020 00:18

Если у кого-то возникла проблема с тем, что всплывающее окно «Сохранить черновик/Удалить черновик» отображается с задержкой, а клавиатура скрывает задержку, мне помогло добавить .edgesIgnoringSafeArea(.bottom) на лист MailView, чтобы решить эту проблему.

sp4c38 27.10.2020 15:01

Он работает, но имеет сбой, например, когда данные передаются пользователю, и мы не можем провести пальцем вниз, чтобы закрыть представление, как это делает реализация UIKit.

Roland Lariotte 22.12.2020 14:46

К сожалению, после того, как анимация отображения представления завершена, шрифт прыгает с одного размера на другой, подобно тому, как представление обновляется до других настроек после завершения анимации. Я также не могу закрыть представление с помощью действия смахивания (только отмена). Хотя тот же MFMailComposeViewController, представленный с UIKit, работает отлично. Кто-нибудь знает причину и решение?

bodich 07.01.2022 19:16

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

Приятное дополнительное прикосновение тщательности!

forrest 09.04.2020 23:46

Здорово! Я хотел бы добавить последний штрих. Как сделать обычный звук при отправке почты?

Nicola Mingotti 15.07.2020 02:41

Я создал для него репозиторий 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.

Ну, у меня есть старый код, который я использовал в 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.

Вау это лучшее. Так просто в использовании. Работал отлично. Просто удалите Utilities.showErrorBanner.

thegathering 28.10.2020 13:18

К сожалению, это решение приводит к сбою приложения при попытке открыть почтовое приложение дважды подряд. Эта ошибка отображается «[PPT] Ошибка при создании CFMessagePort, необходимого для связи с PPT».

Roland Lariotte 21.12.2020 23:33

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
    }

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

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

Хорошее дополнение! Вы правы, не все используют приложение Apple Mail! Что интересно, у меня не установлено приложение Apple Mail, но MFMailComposeViewController.canSendMail() по-прежнему возвращает значение true.

Dom 08.09.2020 21:14

Хм, странно. Если вы удалили почтовое приложение, оно не должно было вернуть значение true.

Stephen Lee 08.09.2020 23:55

это полная копия stackoverflow.com/a/55765362/6898849. пожалуйста, оставьте ссылку на другой ответ, если вы ссылаетесь на него

ramzesenok 14.09.2020 22:49

Это здорово и все такое, но я не могу найти информацию о добавлении файловых вложений для этих сторонних приложений.

TealShift 15.12.2021 22:50

Я также улучшил ответ @Hobbes, чтобы упростить настройку таких параметров, как тема, получатели.

Оформить заказ

Даже лень выписывать суть, тогда как насчет СЗМ?

Теперь вы можете легко копировать и вставлять этот подарок в разные проекты.

Применение;

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]"])
        }
    }
}

вытащить композитор (MFMailComposeViewController) — хороший ход; Спасибо!

Meet Vora 07.03.2022 18:31

Я обновил и упростил ответ @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")
        })

К сожалению, решение @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(["[email protected]"])
    mailController.mailComposeDelegate = self
    present(mailController, animated: true, completion: nil)
}

Сегодня Как разработчик, я хочу уважать выбор пользователем почтового приложения, будь то Mail, Edison, Gmail, Outlook или Hey. Для этого я не могу использовать MFMailComposeViewController. Вместо этого мне нужно добавить почта к ключу 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. Решение похоже на ответ @Mahmud Assan, но с дополнительными параметрами приложения электронной почты и предупреждением приложения об ошибке.

Я заменил некоторый код для метода, позволяющего открывать больше почтовых приложений, а не только Mail или Gmail.

Во-первых, не забудьте добавить соответствующую информацию в Информация.plist, в моем случае:

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

После этого вам нужно создать новый быстрый файл со следующим кодом:

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 protected]"])
        }
    }
}

Другие вопросы по теме