swiftuimfmailcomposeviewcontroller

Properly presenting a MFMailComposeViewController from SwiftUI


As part of converting a hobby project from UIKit to SwiftUI, I'm trying to present a MFMailComposeViewController from a SwiftUI view. It works, but the view controller isn't properly presented from the bottom of the screen as when using UIKit.

For example, following this answer by Hobbes the tige, here's the result I get:

Mail compose view presented by SwiftUI

With the original UIKit code, the same view looks like this:

Mail compose view presented by UIKit

Note how the bottom of the screen looks much worse in the SwiftUI version. Is there any way around this? Or a better way of presenting a MFMailComposeViewController from SwiftUI?

Here's the full code of a test application based on the answer linked above showcasing the problem:

MailComposeTestApp.swift

import SwiftUI

@main
struct MailComposeTestApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

ContentView.swift

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()
        }, label: {
            Label("Send mail", systemImage: "envelope")
        })
        .disabled(!MFMailComposeViewController.canSendMail())
        .sheet(isPresented: $isShowingMailView) {
            MailView(result: self.$result)
        }
    }
}

MailView.swift:

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

Solution

  • Seeing the same issue with the PHPickerViewController confirmed my suspicion that this was a general behavior in SwiftUI, and made the solution easier to track down.

    Turns out you must manually allow the sheet view to spill over into the safe areas at the bottom:

            .sheet(isPresented: $isShowingMailView) {
                MailView(result: self.$result)
                    .edgesIgnoringSafeArea(.bottom)
            }
    

    This seems to work with any type of view presented in this way.