In that older post I discussed about the MFMailComposeViewController in UIKit; a class that allows to present and use a system-provided view controller in order to compose and send emails through our own applications.
In this post today, I'm going to talk about how to integrate MFMailComposeViewController in SwiftUI projects, since there is no native SwiftUI alternative at the time of writing. But that's just the half; the other half is a step by step guidance on how to hide everything behind a custom view modifier, making email composing seem a pure SwiftUI task.
To get a taste, this is what we are going to end up with:
Button("Send Email") {
showEmailComposer = true
}
.emailComposer(isPresented: $showEmailComposer,
emailData: emailData) { result in
// Handle send results.
}
Note
We won't focus on the details of MFMailComposeViewController in this tutorial, as they were all covered in previous post.
The EmailData type
When presenting an MFMailComposeViewController instance, we can optionally provide default content to it. That content may include the subject, one or more recipients, email's body, attachments, and a few more.
In order to handle all that easily, we're going to create a custom type that will keep these content pieces before presenting the email composer. It's a simple struct with a few properties and an inner custom type to represent attachment data:
struct EmailData {
var subject: String = ""
var recipients: [String]?
var body: String = ""
var isBodyHTML = false
var attachments = [AttachmentData]()
struct AttachmentData {
var data: Data
var mimeType: String
var fileName: String
}
}
Note that the data type of each property is in accordance to the data type of the respective property in the MFMailComposeViewController class. EmailData is going to be really helpful, and we'll put it in action right in the next part.
The EmailComposerView
MFMailComposeViewController is a UIKit view controller, therefore we can't use it just like that in SwiftUI. In order to bring it to SwiftUI, it's necessary to create a custom type conforming to UIViewControllerRepresentable protocol. An instance of that new type is what we'll use then in SwiftUI views.
We'll name that custom type EmailComposerView. According to the UIViewControllerRepresentable protocol requirements, it's mandatory to implement the following two methods:
struct EmailComposerView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> MFMailComposeViewController {
}
func updateUIViewController(_ uiViewController: MFMailComposeViewController,
context: Context) { }
}
The first method is the place to initialize, configure, and return an MFMailComposeViewController instance. The second is where changes coming from SwiftUI trigger updates to the view controller. We are not going to need it here, so we'll leave it empty.
Important
Import the MessageUI framework at the top of the file to access the MFMailComposeViewController class:
import MessageUI
Before we proceed to the code of makeUIViewController(context:), let's declare the following properties first:
struct EmailComposerView: UIViewControllerRepresentable {
@Environment(\.presentationMode) private var presentationMode
let emailData: EmailData
var result: (Result<MFMailComposeResult, Error>) -> Void
...
}
Here's the purpose of them:
- An
EmailComposerViewinstance will be presented modally in a sheet, and thepresentationModeenvironment property will let us dismiss it easily. - The
emailDatawill contain any predefined values and data to feed theMFMailComposeViewControllerinstance with. - The
resultproperty is a closure that will contain either the email sending result, or any error that was potentially occurred. Note thatMFMailComposeResultis an enum with the following cases regarding an email: sent, saved, cancelled, failed. They are allIntvalues.
With all that in place, let's focus on the first method now. We'll begin by initializing a MFMailComposeViewController object:
func makeUIViewController(context: Context) -> MFMailComposeViewController {
let emailComposer = MFMailComposeViewController()
}
Next, we'll set the delegate object, through which we'll receive messages from MFMailComposeViewController. This cannot be the self instance; EmailComposerView is a value type, and the delegate object must be a reference type, or in other words, a class instance.
To manage that in UIViewControllerRepresentable types, it's necessary to define a Coordinator class that conforms to delegate protocols and implements the various delegate methods. We'll do that in a moment.
Accessing an object of that class in the makeUIViewController(context:) method is done through the context parameter value, and we'll use it right now in order to set the mailComposeDelegate in the emailComposer instance:
func makeUIViewController(context: Context) -> MFMailComposeViewController {
...
emailComposer.mailComposeDelegate = context.coordinator
}
Before we return the emailComposer object from the method, we'll provide it with all potential predefined content using the emailData object like so:
func makeUIViewController(context: Context) -> MFMailComposeViewController {
...
emailComposer.setSubject(emailData.subject)
emailComposer.setToRecipients(emailData.recipients)
emailComposer.setMessageBody(emailData.body, isHTML: emailData.isBodyHTML)
for attachment in emailData.attachments {
emailComposer.addAttachmentData(attachment.data, mimeType: attachment.mimeType, fileName: attachment.fileName)
}
}
Finally, we return the emailComposer object. The entire method is this:
func makeUIViewController(context: Context) -> MFMailComposeViewController {
let emailComposer = MFMailComposeViewController()
emailComposer.mailComposeDelegate = context.coordinator
emailComposer.setSubject(emailData.subject)
emailComposer.setToRecipients(emailData.recipients)
emailComposer.setMessageBody(emailData.body, isHTML: emailData.isBodyHTML)
for attachment in emailData.attachments {
emailComposer.addAttachmentData(attachment.data, mimeType: attachment.mimeType, fileName: attachment.fileName)
}
return emailComposer
}
Before moving on, let's also define a static method that will indicate whether a device can actually send emails or not. That's an optional step, but it will help keep everything around our custom type:
struct EmailComposerView: UIViewControllerRepresentable {
...
static func canSendEmail() -> Bool {
MFMailComposeViewController.canSendMail()
}
}
The Coordinator class
The next step is to define the Coordinator class in the EmailComposerView; it will deal with delegate messages coming from the UIKit part.
struct EmailComposerView: UIViewControllerRepresentable {
...
class Coordinator: NSObject, MFMailComposeViewControllerDelegate {
var parent: EmailComposerView
init(_ parent: EmailComposerView) {
self.parent = parent
}
}
}
Notice that Coordinator inherits from NSObject and adopts the MFMailComposeViewControllerDelegate. The parent property is the EmailComposerView instance that is provided to the class upon initialization.
There is just one delegate method to implement. In it, we'll check whether an error occurred or not. If so, then we'll call the result closure of parent, indicating a failure and passing the error.
In the opposite case, we'll call the result again, but indicating success and passing the compose result this time. In any case, we'll dismiss the sheet that contains the mail composer instance using the presentationMode environment property.
Here is all that:
class Coordinator: NSObject, MFMailComposeViewControllerDelegate {
...
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
if let error = error {
parent.result(.failure(error))
return
}
parent.result(.success(result))
parent.presentationMode.wrappedValue.dismiss()
}
}
The Coordinator class is now complete. There is one last thing missing in the EmailComposerView; to create a Coordinator instance. We do so in another method:
struct EmailComposerView: UIViewControllerRepresentable {
...
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
}
The EmailComposerView custom type is now complete! Let's go ahead to make a first use of it, and then see how to create a custom view modifier in order to make things easier and more elegant.
Note
Read about the UIViewControllerRepresentable protocol if you are not familiar it.
Using the EmailComposerView
Suppose that we have the following SwiftUI view with some initial implementation:
struct ContentView: View {
@State private var showEmailComposer = false
@State private var showAlert = false
@State private var alertMessage: String = ""
let emailData = EmailData(subject: "Hi there!", recipients: ["gabriel@serialcoder.dev"])
var body: some View {
Button("Send Email") {
}
.sheet(isPresented: $showEmailComposer, content: {
})
.alert(isPresented: $showAlert, content: {
Alert(title: Text("Send Email"),
message: Text(alertMessage),
dismissButton: .default(Text("Dismiss")))
})
}
}
The purpose of the view is to present the email composer when tapping on the Send Email button. The sheet modifier exists because it's going to contain an EmailComposerView instance as its content.
See that there is also an alert; we need it to display a message to the user if the device cannot send an email. In this particular demo, alert has an additional role. We'll use it to report back a successful or failed email sending.
Additionally, there are three state properties in the above snippet. In order of appearance, they indicate and define the presented state of both the sheet and alert, and keep the message that the alert will display. We also create an EmailData instance with a couple of default values; the subject and the recipient.
Now, we need to do two things in order to present the email composer. The first is to present the sheet in the button's action closure. However, we'll do so after we make sure that the device can actually send emails. If it can't, then we'll show the alert instead:
Button("Send Email") {
if EmailComposerView.canSendEmail() {
showEmailComposer = true
} else {
alertMessage = "Unable to send an email from this device."
showAlert = true
}
}
The second thing is to initialize an EmailComposerView instance in the sheet's content:
.sheet(isPresented: $showEmailComposer, content: {
EmailComposerView(emailData: emailData) { result in
handleEmailComposeResult(result)
}
})
See that we supply the emailData property as argument to the EmailComposerView. Besides that, handleEmailComposeResult(_:) is a custom method, handling result in a way specific to this example. Most probably, a different handling would be more appropriate in a real application. The handleEmailComposerResult(_:) method is this:
func handleEmailComposeResult(_ result: Result<MFMailComposeResult, Error>) {
switch result {
case .success(let result):
let resultString = ["Cancelled", "Saved", "Sent", "Failed"][result.rawValue]
alertMessage = "Email result: \(resultString)"
case .failure(let error):
alertMessage = "Failed to send email.\n\(error.localizedDescription)"
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
showAlert = true
}
}
After all the above, the entire implementation of the view is now this:
struct ContentView: View {
@State private var showEmailComposer = false
@State private var showAlert = false
@State private var alertMessage: String = ""
let emailData = EmailData(subject: "Hi there!", recipients: ["gabriel@serialcoder.dev"])
var body: some View {
Button("Send Email") {
if EmailComposerView.canSendEmail() {
showEmailComposer = true
} else {
alertMessage = "Unable to send an email from this device."
showAlert = true
}
}
.sheet(isPresented: $showEmailComposer, content: {
EmailComposerView(emailData: emailData) { result in
handleEmailComposeResult(result)
}
})
.alert(isPresented: $showAlert, content: {
Alert(title: Text("Send Email"),
message: Text(alertMessage),
dismissButton: .default(Text("Dismiss")))
})
}
}
The above is the shortest implementation we can do in order to present the MFMailComposeViewController through the EmailComposerView in SwiftUI. It's simple, but not easily reusable.
So, let's keep going to find out how to make things simpler and easier to reuse with a custom view modifier.
Creating a custom view modifier
You can think of the view modifier that we'll create here, as a special kind of a sheet. That's because we are going to actually contain a sheet in the modifier, and keep the EmailComposerView instance as the sheet's content.
Having that information in mind, we'll start implementing a new custom type that will conform to the ViewModifier protocol. We'll declare a few properties first that we can group in two categories; those necessary to the sheet, and those necessary to the EmailComposerView:
struct EmailComposer: ViewModifier {
@Binding var isPresented: Bool
var emailData: EmailData
var onDismiss: (() -> Void)? = nil
var result: (Result<MFMailComposeResult, Error>) -> Void
}
Notice that there is an optional onDismiss parameter declared among the others. With it, we can initialize a sheet passing optionally a closure as argument to it, and perform potential additional actions upon sheet's dismissal.
Next, we'll implement the required (by the ViewModifier protocol) body(content:) method. In the content parameter value we'll apply the sheet modifier. However, note something important here; we will not just contain an EmailComposerView instance into the sheet. We'll check if the device can send an email, and if not, then we'll display a Text view with a relevant message and a dismiss button instead!
There is a reason for doing that. That way, we'll manage to get rid of the alert in the SwiftUI view if we don't need it for any other purpose. So, eventually, the sheet is going to have a double role. To display either the EmailComposerView, or a Text view along with a button.
Here is the implementation:
struct EmailComposer: ViewModifier {
...
func body(content: Content) -> some View {
content
.sheet(isPresented: $isPresented, onDismiss: onDismiss) {
if EmailComposerView.canSendEmail() {
EmailComposerView(emailData: emailData) { result in
self.result(result)
}
} else {
VStack {
Text("Unable to send an email from this device.")
Button("Dismiss") {
isPresented = false
}
}
}
}
}
}
See that in case that the device can send emails, we're passing the EmailComposerView's result as argument to the result of the EmailComposer instance. Otherwise, we're setting false to the isPresented binding property that controls the presented state of the sheet inside the dismiss button's action closure.
We can now use the above as shown right next:
var body: some View {
Button("Send Email") {
showEmailComposer = true
}
.modifier(EmailComposer(isPresented: $showEmailComposer,
emailData: emailData,
onDismiss: nil, result: { result in
handleEmailComposeResult(result)
}))
}
In the button's action we don't check anymore whether the device can send emails or not; we handle that in the EmailComposer view modifier. All we need is to make the showEmailComposer property true.
But the most important part is how we use the EmailComposer modifier. The only way to put it in action is by passing it as argument to the modifier() view modifier.
Even though the above is undoubtedly simpler and shorter compared to the initial implementation, it is still not so elegant. And that's because we have to call modifier() whenever we want to use the EmailComposer view modifier.
As a last step in this post, let's change that and provide a more natural approach.
The emailComposer method
In order to create a view modifier that looks like built-in modifiers in SwiftUI, defining a ViewModifier type (such as the EmailComposer) is not enough. It's also necessary to extend the View protocol and define a new method.
The name of that method should be exactly what we want to use eventually as a custom modifier.
To showcase that, here is a View protocol's extension with a new method defined in it:
extension View {
func emailComposer(isPresented: Binding<Bool>,
emailData: EmailData,
onDismiss: (() -> Void)? = nil,
result: @escaping (Result<MFMailComposeResult, Error>) -> Void) -> some View {
}
}
The method's parameter values are exactly the same to the properties of the EmailComposer type. That's because in its body we are going to call the the modifier() view modifier and pass an EmailComposer instance:
extension View {
func emailComposer(isPresented: Binding<Bool>,
emailData: EmailData,
onDismiss: (() -> Void)? = nil,
result: @escaping (Result<MFMailComposeResult, Error>) -> Void) -> some View {
self.modifier(EmailComposer(isPresented: isPresented,
emailData: emailData,
onDismiss: onDismiss,
result: result))
}
}
We can now call the above method like any other view modifier in SwiftUI:
Button("Send Email") {
showEmailComposer = true
}
.emailComposer(isPresented: $showEmailComposer,
emailData: emailData, onDismiss: nil) { result in
handleEmailComposeResult(result)
}
In fact, we can even omit the onDismiss argument at all if we don't want to perform any actions upon the sheet's dismissal. Remember that it's nil by default:
Button("Send Email") {
showEmailComposer = true
}
.emailComposer(isPresented: $showEmailComposer, emailData: emailData) { result in
handleEmailComposeResult(result)
}
That's definitely a much better implementation than what we started with!
Summary
Composing and sending emails is a common feature in applications. Doing so is not difficult with the use of the MFMailComposeViewController class. However, that's a UIKit view controller, and it takes some additional steps in order to make it available as a view in SwiftUI. In this post I went through those steps, and I showed how to implement a UIViewControllerRepresentable type and end up with a SwiftUI view. After that, I demonstrated how to take that view and hide it behind a custom view modifier, and eventually have a neat, handy and SwiftUI-like way to present the email composer. I hope that you've found all those interesting and useful. Thank you for reading!