iosswiftmfmailcomposeviewcontrollerclass-hierarchymessageui

Swift 5 - Email Class Helper / Manager


Edit:

Big thanks to Paulw11 for helping me solve this issue. I've added the full code here for easy reuse:

Class:

import UIKit
import MessageUI

struct Feedback {
    let recipients: [String]
    let subject: String
    let body: String
    let footer: String
}

class FeedbackManager: NSObject, MFMailComposeViewControllerDelegate {

private var feedback: Feedback

private var completion: ((Result<MFMailComposeResult,Error>)->Void)?

override init() {
    fatalError("Use FeedbackManager(feedback:)")
}

init?(feedback: Feedback) {
    guard MFMailComposeViewController.canSendMail() else {
        return nil
    }
    
    self.feedback = feedback
}

func send(on viewController: UIViewController, completion:(@escaping(Result<MFMailComposeResult,Error>)->Void)) {
    
    let mailVC = MFMailComposeViewController()
    self.completion = completion
    
    mailVC.mailComposeDelegate = self
    mailVC.setToRecipients(feedback.recipients)
    mailVC.setSubject(feedback.subject)
    mailVC.setMessageBody("<p>\(feedback.body)<br><br><br><br><br>\(feedback.footer)</p>", isHTML: true)
    
    viewController.present(mailVC, animated:true)
}

func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
    if let error = error {
        completion?(.failure(error))
        controller.dismiss(animated: true)
    } else {
        completion?(.success(result))
        controller.dismiss(animated: true)
    }
}
}

In View Controller:

Add Variable:

var feedbackManager: FeedbackManager?

Use:

    let feedback = Feedback(recipients: "String", subject: "String", body: "Body", footer: "String")
    if let feedManager = FeedbackManager(feedback: feedback) {
        self.feedbackManager = feedManager
        self.feedbackManager?.send(on: self) { [weak self] result in
            switch result {
            case .failure(let error):
                print("error: ", error.localizedDescription)
            // Do something with the error
            case .success(let mailResult):
                print("Success")
                // Do something with the result
            }
            self?.feedbackManager = nil
        }
    } else { // Cant Send Email: // Added UI Alert:
        let failedMenu = UIAlertController(title: "String", message: nil, preferredStyle: .alert)
        let okAlert = UIAlertAction(title: "String", style: .default)
        failedMenu.addAction(okAlert)
        present(failedMenu, animated: true)
    }

I'm trying to make a class that handles initializing a MFMailComposeViewController to send an email inside of the app.

I'm having issues making it work. Well, rather making it not crash if it doesn't work.

class:

import UIKit
import MessageUI

struct Feedback {
    let recipients = "String"
    let subject: String
    let body: String
}

class FeedbackManager: MFMailComposeViewController, MFMailComposeViewControllerDelegate {
    
    func sendEmail(feedback: Feedback) {
        
        if MFMailComposeViewController.canSendMail() {
            
            self.mailComposeDelegate = self
            self.setToRecipients([feedback.recipients])
            self.setSubject("Feedback: \(feedback.subject)")
            self.setMessageBody("<p>\(feedback.body)</p>", isHTML: true)
            
        } else {
            print("else:")
            mailFailed()
        }
    }
    
    func mailFailed() {
        print("mailFailed():")
        let failedMenu = UIAlertController(title: "Please Email Me!", message: nil, preferredStyle: .alert)
        let okAlert = UIAlertAction(title: "Ok!", style: .default)
        failedMenu.addAction(okAlert)
        self.present(failedMenu, animated: true, completion: nil)
    }
    
    func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
        controller.dismiss(animated: true)
    }
}

And then calling it from a different view controller:

  let feedbackManager = FeedbackManager()
  feedbackManager.sendEmail(feedback: Feedback(subject: "String", body: "String"))
  self.present(feedbackManager, animated: true, completion: nil)
  tableView.deselectRow(at: indexPath, animated: true)

The above works just fine if MFMailComposeViewController.canSendMail() == true. The problem I'm facing is that if canSendMail() is not true, then the class obviously cant initialize and crashes. Which makes sense.

Error:

Unable to initialize due to + [MFMailComposeViewController canSendMail] returns NO.

I'm not sure where to go from here on how to get this working. I've tried changing FeedbackManager from MFMailComposeViewController to a UIViewController. And that seems to work but because it's adding a view on the stack, it's causing a weird graphical display.

The other thing I could do is import MessageUI, and conform to MFMailComposeViewController for every controller I want to be able to send an email from. So that I can check against canSendMail() before trying to initialize FeedbackManager(). But that also doesn't seem like the best answer.

How else can I get this working?

EDIT: I've gotten the code to work with this however, there is an ugly transition with the addition of the view onto the stack before it presents the MFMailComposeViewController.

class FeedbackManager: UIViewController, MFMailComposeViewControllerDelegate {
    
    func sendEmail(feedback: Feedback, presentingViewController: UIViewController) -> UIViewController {
        
        if MFMailComposeViewController.canSendMail() {
            
            let mail = MFMailComposeViewController()
            mail.mailComposeDelegate = self
            mail.setToRecipients([feedback.recipients])
            mail.setSubject("Feedback: \(feedback.subject)")
            mail.setMessageBody("<p>\(feedback.body)</p>", isHTML: true)
            
             present(mail, animated: true)
            return self
        } else {
            print("else:")
            return mailFailed(presentingViewController: presentingViewController)
        }
    }
    
    func mailFailed(presentingViewController: UIViewController) -> UIViewController {
        print("mailFailed():")
        let failedMenu = UIAlertController(title: "Please Email Me!", message: nil, preferredStyle: .alert)
        let okAlert = UIAlertAction(title: "Ok!", style: .default)
        failedMenu.addAction(okAlert)
        return failedMenu
    }
    
    func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
        controller.dismiss(animated: true)
        self.dismiss(animated: false)
    }
}

Solution

  • Subclassing MFMailComposeViewController is the wrong approach. This class is intended to be used "as-is". You can build a wrapper class if you like:

    struct Feedback {
        let recipients = "String"
        let subject: String
        let body: String
    }
    
    class FeedbackManager: NSObject, MFMailComposeViewControllerDelegate {
        
        private var feedback: Feedback
        
        private var completion: ((Result<MFMailComposeResult,Error>)->Void)?
        
        override init() {
            fatalError("Use FeedbackManager(feedback:)")
        }
        
        init?(feedback: Feedback) {
            guard MFMailComposeViewController.canSendMail() else {
                return nil
            }
            
            self.feedback = feedback
        }
        
        func send(on viewController: UIViewController, completion:(@escaping(Result<MFMailComposeResult,Error>)->Void)) {
            
            let mailVC = MFMailComposeViewController()
            self.completion = completion
            
            mailVC.mailComposeDelegate = self
            mailVC.setToRecipients([feedback.recipients])
            mailVC.setSubject("Feedback: \(feedback.subject)")
            mailVC.setMessageBody("<p>\(feedback.body)</p>", isHTML: true)
            
            viewController.present(mailVC, animated:true)
        } 
        
        func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
            if let error = error {
                completion?(.failure(error))
            } else {
                completion?(.success(result))
            }
        }
    }
    

    And then to use it from a view controller:

    
    let feedback = Feedback(subject: "String", body: "Body")
    if let feedbackMgr = FeedbackManager(feedback: feedback) {
        self.feedbackManager = feedbackMgr
        feedback.send(on: self) { [weak self], result in 
            switch result {
                case .failure(let error):
                    // Do something with the error
                case .success(let mailResult):
                    // Do something with the result
            }
            self.feedbackManager = nil
        }
    } else {
        // Can't send email
    }
    

    You will need to hold a strong reference to the FeedbackManager in a property otherwise it will be released as soon as the containing function exits. My code above refers to a property

    var feedbackManager: FeedbackManager?
    

    While this will work, a better UX is if you check canSendMail directly and disable/hide the UI component that allows them to send feedback