I am using MFMailComposeViewController to allow users to share in my app. Now my app is swiftUI so I had to wrap it in UIViewRepresentable. I have tied my mailComposeDelegate for dismissing as well.
struct MailComposerView: UIViewControllerRepresentable {
@Environment(\.presentationMode) var presentation
@Binding var result: Result<MFMailComposeResult, Error>?
var mailControllerWrapperBuilder: () -> MFMailComposerControllerWrappable = { MFMailComposeViewController() }
let subject: String
let body: String
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 {
if let error = error {
self.result = .failure(error)
} else {
self.result = .failure(NSError())
}
return
}
self.result = .success(result)
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(presentation: presentation,
result: $result)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<MailComposerView>) -> UIViewController {
var mailComposer = mailControllerWrapperBuilder()
mailComposer.setSubject(subject)
mailComposer.setMessageBody(body, isHTML: false)
mailComposer.mailComposeDelegate = context.coordinator
return mailComposer.getUIViewController()
}
func updateUIViewController(_ uiViewController: UIViewController,
context: UIViewControllerRepresentableContext<MailComposerView>) { }
}
protocol MFMailComposerControllerWrappable {
var mailComposeDelegate: MFMailComposeViewControllerDelegate? { get set }
var delegate: UINavigationControllerDelegate? { get set }
func setSubject(_ subject: String)
func setMessageBody(_ body: String, isHTML: Bool)
func getUIViewController() -> UIViewController
}
extension MFMailComposeViewController: MFMailComposerControllerWrappable {
func getUIViewController() -> UIViewController { self }
}
On my main view, which is a swiftui sheet, I present mail composer as a sheet.
@State var showMail = false
var body: some View {
VStack {
Button("Mail", action: {
showMail = true
})
}
.sheet(isPresented: $showMail, content: {
MailComposerView(result: $viewModel.mailResult, subject: "subject", body: "body")
.presentationDetents([.large])
})
}
On iOS 17 when swiping down, interactive dismiss is activated and shows user if they want to cancel sending mail.
While on iOS16 this behaviour is not observed.
I have tried following
A custom swiftui view in sheet has interactive dismiss
UIViewRespresentable of custom uiview has interactive dismiss
Seems like this bug has to do with MFMailComposeViewController itself.
Is there a known issue that was fixed in iOS 17? On other apple apps, this behaviour is not observed.
One fix that I have on my mind is to present mailcomposer via rootcontroller and not rely on swiftui sheet.