Reading time: 2 min

This recipe shows how to send mail from SwiftUI. You'll build a MailView by wrapping MessageUI's MFMailComposeViewController in a UIViewControllerRepresentable. You'll be able to:

  • Determine if you can send mail or not.
  • Pass subject, message and recipients to the view via a binding.
  • Attach files to the email.
  • Receive success or failure result after sending the email.

The end result will look like this.

ezgif-3-c21e985ad818

This component is available as a Swift Package in this repo.

OK, first define the models for the data you'll pass to the MailView. These correspond to parameters that you can set on a MFMailComposeViewController:

struct ComposeMailData {
  let subject: String
  let recipients: [String]?
  let message: String
  let attachments: [AttachmentData]?
}

struct AttachmentData {
  let data: Data
  let mimeType: String
  let fileName: String
}

Then, add the wrapper code:

import SwiftUI
import UIKit
import MessageUI

typealias MailViewCallback = ((Result<MFMailComposeResult, Error>) -> Void)?

struct MailView: UIViewControllerRepresentable {
  @Environment(\.presentationMode) var presentation
  @Binding var data: ComposeMailData
  let callback: MailViewCallback

  class Coordinator: NSObject, MFMailComposeViewControllerDelegate {
    @Binding var presentation: PresentationMode
    @Binding var data: ComposeMailData
    let callback: MailViewCallback

    init(presentation: Binding<PresentationMode>,
         data: Binding<ComposeMailData>,
         callback: MailViewCallback) {
      _presentation = presentation
      _data = data
      self.callback = callback
    }

    func mailComposeController(_ controller: MFMailComposeViewController,
                               didFinishWith result: MFMailComposeResult,
                               error: Error?) {
      if let error = error {
        callback?(.failure(error))
      } else {
        callback?(.success(result))
      }
      $presentation.wrappedValue.dismiss()
    }
  }

  func makeCoordinator() -> Coordinator {
    Coordinator(presentation: presentation, data: $data, callback: callback)
  }

  func makeUIViewController(context: UIViewControllerRepresentableContext<MailView>) -> MFMailComposeViewController {
    let vc = MFMailComposeViewController()
    vc.mailComposeDelegate = context.coordinator
    vc.setSubject(data.subject)
    vc.setToRecipients(data.recipients)
    vc.setMessageBody(data.message, isHTML: false)
    data.attachments?.forEach {
      vc.addAttachmentData($0.data, mimeType: $0.mimeType, fileName: $0.fileName)
    }
    vc.accessibilityElementDidLoseFocus()
    return vc
  }

  func updateUIViewController(_ uiViewController: MFMailComposeViewController,
                              context: UIViewControllerRepresentableContext<MailView>) {
  }

  static var canSendMail: Bool {
    MFMailComposeViewController.canSendMail()
  }
}

That's it! You can now use your new MailView component like this:

struct MailViewTest: View {
  @State private var mailData = ComposeMailData(subject: "A subject",
                                                recipients: ["i.love@swiftuirecipes.com"],
                                                message: "Here's a message",
                                                attachments: [AttachmentData(data: "Some text".data(using: .utf8)!,
                                                                             mimeType: "text/plain",
                                                                             fileName: "text.txt")
                                               ])
 @State private var showMailView = false

  var body: some View {
    Button(action: {
      showMailView.toggle()
    }) {
      Text("Send mail")
    }.disabled(!MailView.canSendMail)
    .sheet(isPresented: $showMailView) {
      MailView(data: $mailData) { result in
        print(result)
       }
    }
  }
}

Next Post Previous Post