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:
With the original UIKit code, the same view looks like this:
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>) {
}
}
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.