Both MFMailComposeViewController and MFMessageComposeViewController giving crash on Cancel button - ios

I have created a utility class for sending mail and sms. This is the complete code.
class Utility: NSObject, MFMailComposeViewControllerDelegate, MFMessageComposeViewControllerDelegate
{
var subject: String!
var body: String!
var to: [String]!
var cc: [String]!
var viewController: UIViewController!
// MARK: Email utility
/// sendEmail : Sends email
///
/// - Parameters:
/// - vc: UIViewController from which mail is to be sent
/// - subj: Email subject
/// - emailBody: Email body
/// - toRecipients: To recipients
/// - ccRecipients: CC recipients
func sendEmail(vc: UIViewController, subj: String, emailBody: String, toRecipients: [String], ccRecipients: [String])
{
self.subject = subj
self.body = emailBody
self.to = toRecipients
self.cc = ccRecipients
self.viewController = vc
let mailComposeViewController = configuredMailComposeViewController()
if MFMailComposeViewController.canSendMail() {
self.viewController.present(mailComposeViewController, animated: true, completion: nil)
} else {
self.showSendMailErrorAlert()
}
}
/// Show error alert when email could not be sent
func showSendMailErrorAlert() {
let alert = UIAlertController(title: "Could Not Send Email", message: "Your device could not send e-mail. Please check e-mail configuration and try again.", preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: "Ok", style: UIAlertActionStyle.default, handler: nil))
self.viewController.present(alert, animated: true, completion: nil)
}
/// configuredMailComposeViewController : Set up mail compose view controller
///
/// - Returns: <#return value description#>
func configuredMailComposeViewController() -> MFMailComposeViewController {
let mailComposerVC = MFMailComposeViewController()
mailComposerVC.mailComposeDelegate = self
mailComposerVC.setToRecipients(self.to)
mailComposerVC.setCcRecipients(self.cc)
mailComposerVC.setSubject(self.subject)
mailComposerVC.setMessageBody(self.body, isHTML: false)
return mailComposerVC
}
// MARK: MFMailComposeViewControllerDelegate
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
controller.dismiss(animated: true, completion: nil)
}
// MARK: END
// MARK: SMS utility
func sendSMS(vc: UIViewController, content: String, phoneNumber: String)
{
self.viewController = vc
if (MFMessageComposeViewController.canSendText()) {
let messageController = MFMessageComposeViewController()
messageController.body = content
messageController.recipients = [phoneNumber]
messageController.messageComposeDelegate = self
self.viewController.present(messageController, animated: true, completion: nil)
}
}
func messageComposeViewController(_ controller: MFMessageComposeViewController, didFinishWith result: MessageComposeResult) {
//... handle sms screen actions
controller.dismiss(animated: true, completion: nil)
}
// MARK: END
Cancel button on both features is giving crash. As a matter of fact, if I put breakpoint on didFinishWith for both features, the function is not called and crash appears before that.
Strange thing is, if I write this code in UIViewController class, it works fine.

Related

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)
}
}
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
Solved this by first adding a class that checks if .canSendMail is true. If it is, it then taps into the postal sending class to present the MFMailComposeViewController.
This is the only workaround I've come up with that allows MFMailComposeViewController to be it's own MFMailComposeViewControllerDelegate. While also preventing a crash if .canSendMail = false.
import UIKit
import MessageUI
struct Feedback {
let recipients = ["Strings"]
let subject: String
let body: String
}
class FeedbackManager {
func tryMail() -> Bool {
if MFMailComposeViewController.canSendMail() {
return true
} else {
return false
}
}
func mailFailed() -> UIViewController {
let failedMenu = UIAlertController(title: "Please Email Me!", message: nil, preferredStyle: .alert)
let okAlert = UIAlertAction(title: "Ok!", style: .default)
failedMenu.addAction(okAlert)
return failedMenu
}
}
class PostalManager: MFMailComposeViewController, MFMailComposeViewControllerDelegate {
func sendEmail(feedback: Feedback) -> MFMailComposeViewController {
if MFMailComposeViewController.canSendMail() {
self.mailComposeDelegate = self
self.setToRecipients(feedback.recipients)
self.setSubject("Feedback: \(feedback.subject)")
self.setMessageBody("<p>\(feedback.body)</p>", isHTML: true)
}
return self
}
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
controller.dismiss(animated: true)
}
}
Called with:
let feedbackManager = FeedbackManager()
let feedback = Feedback(subject: "String", body: "Body")
switch feedbackManager.tryMail() {
case true:
let postalManager = PostalManager()
present(postalManager.sendEmail(feedback: feedback), animated: true)
case false:
present(feedbackManager.mailFailed(), animated: true)
}
You can change the code as follows.
struct Feedback {
let recipients = "String"
let subject: String
let body: String
}
class FeedbackManager: NSObject, MFMailComposeViewControllerDelegate {
func sendEmail(presentingViewController: 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)
presentingViewController.present(mail, animated: true)
} else {
print("else:")
mailFailed(presentingViewController: presentingViewController)
}
}
func mailFailed(presentingViewController: UIViewController) {
print("mailFailed():")
let failedMenu = UIAlertController(title: "Please Email Me!", message: nil, preferredStyle: .alert)
let okAlert = UIAlertAction(title: "Ok!", style: .default)
failedMenu.addAction(okAlert)
presentingViewController.present(failedMenu, animated: true, completion: nil)
}
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
controller.dismiss(animated: true)
}
}
Now, mailComposer can be opened as follows from another UIViewController class.
let feedbackManager = FeedbackManager()
feedbackManager.sendEmail(presentingViewController: self)
Hope it helps

Send Mail with button Error

I'm having an error with my code. I know there are a lot of tutorials on how to make a button send an email in swift, but I don't understand what's wrong with my code. Can someone help explain what I'm doing wrong? Thanks.
import UIKit
import MessageUI
class AboutUsVC: UIViewController, MFMessageComposeViewControllerDelegate {
func messageComposeViewController(_ controller: MFMessageComposeViewController, didFinishWith result: MessageComposeResult) {
}
func configureMailController() -> MFMessageComposeViewController {
let mailComposerVC = MFMailComposeViewController()
mailComposerVC.mailComposeDelegate = self as? MFMailComposeViewControllerDelegate
mailComposerVC.setToRecipients(["Test#gmail.com"])
mailComposerVC.setSubject("App - Help Contact")
return mailComposerVC()
}
func showMailError() {
let sendMailErrorAlert = UIAlertController(title: "Sorry, couldn't send", message: "Sorry, we are having some troubles sending the message right now. :(", preferredStyle: .alert)
let dismiss = UIAlertAction(title: "Ok", style: .default, handler: nil)
sendMailErrorAlert.addAction(dismiss)
self.present(sendMailErrorAlert, animated: true, completion: nil)
}
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
controller.dismiss(animated: true, completion: nil)
}
In your code, change below code
func configureMailController() -> MFMessageComposeViewController {
let mailComposerVC = MFMailComposeViewController()
mailComposerVC.mailComposeDelegate = self as? MFMailComposeViewControllerDelegate
mailComposerVC.setToRecipients(["Test#gmail.com"])
mailComposerVC.setSubject("App - Help Contact")
return mailComposerVC()
}
with
func configureMailController() -> MFMailComposeViewController {
let mailComposerVC = MFMailComposeViewController()
mailComposerVC.mailComposeDelegate = self
mailComposerVC.setToRecipients(["contact.Studio228#gmail.com"])
mailComposerVC.setSubject("In-Dose - Help Contact")
return mailComposerVC
}

How do I send email on swift?

I have followed peoples code on sending an email on swift and the problem is on my phone, the send button is greyed out. What do I do? Here is my code and a picture of the problem on my phone.
The Problem On My Phone, The Send Button Is Greyed Out.
import UIKit
import MessageUI
class SendEmailViewController: UIViewController, MFMailComposeViewControllerDelegate {
#IBAction func sendEmail(_ sender: Any)
{
let mailComposeViewController = configureMailController()
if MFMailComposeViewController.canSendMail() {
self.present(mailComposeViewController, animated: true, completion: nil)
} else {
showMailError()
}
}
func configureMailController() -> MFMailComposeViewController
{
let mailComposerVC = MFMailComposeViewController()
mailComposerVC.mailComposeDelegate = self
mailComposerVC.setToRecipients(["abc#gmail.com"])
mailComposerVC.setSubject("Gamifiction")
mailComposerVC.setMessageBody("Hey, Check Out My Game", isHTML: false)
return mailComposerVC }
func showMailError()
{
let sendMailErrorAlert = UIAlertController(title: "Could not send email", message: "Your device could not send email", preferredStyle: .alert)
let dismiss = UIAlertAction(title: "Ok", style: .default, handler: nil)
sendMailErrorAlert.addAction(dismiss)
self.present(sendMailErrorAlert, animated: true, completion: nil)
}
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
controller.dismiss(animated: true, completion: nil)
}
}

button from app opens email but won't close the window and return to app

I have a button in my app which opens up an email to be sent to me, when this button is pressed the email app on iPhone opens up and when sent is pressed the email is sent however the window doesn't close and then return to my app. Also when i press cancel it gives the option to save/delete draft but again doesn't close the window and return to my app. I have attached the email code below.
#IBAction func SendMessage(sender: AnyObject) {
var mail: MFMailComposeViewController!
let toRecipients = ["usalim76#gmail.com"]
let subject = "Enquiry"
let body = "Your body text"
mail = MFMailComposeViewController()
mail.mailComposeDelegate = self
mail.setToRecipients(toRecipients)
mail.setSubject(subject)
mail.setMessageBody(body, isHTML: true)
presentViewController(mail, animated: true, completion: nil)
func mailComposeController(controller: MFMailComposeViewController, didFinishWithResult result: MFMailComposeResult, error: NSError?) {
controller.presentingViewController?.dismissViewControllerAnimated(true, completion: nil)
}
}
looks like you forgot to implement the MFMailComposeViewControllerDelegate, add this:
func mailComposeController(controller: MFMailComposeViewController, didFinishWithResult result: MFMailComposeResult, error: NSError?) {
controller.presentingViewController?.dismissViewControllerAnimated(true, completion: nil)
}
Managed to fix it!
#IBAction func SendMessage(sender: AnyObject) {
let mailComposeViewController = configuredMailComposeViewController()
if MFMailComposeViewController.canSendMail() {
self.presentViewController(mailComposeViewController, animated: true, completion: nil)
} else {
self.showSendMailErrorAlert()
}
}
func configuredMailComposeViewController() -> MFMailComposeViewController {
let mailComposerVC = MFMailComposeViewController()
mailComposerVC.mailComposeDelegate = self // Extremely important to set the --mailComposeDelegate-- property, NOT the --delegate-- property
mailComposerVC.setToRecipients(["someone#somewhere.com"])
mailComposerVC.setSubject("Sending you an in-app e-mail...")
mailComposerVC.setMessageBody("Sending e-mail in-app is not so bad!", isHTML: false)
return mailComposerVC
}
func showSendMailErrorAlert() {
let sendMailErrorAlert = UIAlertView(title: "Could Not Send Email", message: "Your device could not send e-mail. Please check e-mail configuration and try again.", delegate: self, cancelButtonTitle: "OK")
sendMailErrorAlert.show()
}
// MARK: MFMailComposeViewControllerDelegate Method
func mailComposeController(controller: MFMailComposeViewController!, didFinishWithResult result: MFMailComposeResult, error: NSError!) {
controller.dismissViewControllerAnimated(true, completion: nil)
}
}

iOS 8 Swift 1.2 and Parse - Attempt to present UIAlertController on ViewController whose view is not in the window hierarchy

There are many questions on Stack concerning this issue, but none of them seem to solve the issue I'm having.
I am using ParseUI for the login and signup portion of my application. What I would like to have happen is for a UIAlertController to be presented when a user (for example) does not enter in any text in the username and password fields.
Here is the code for my MasterViewController:
class MasterViewController: UIViewController, PFLogInViewControllerDelegate,
PFSignUpViewControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
if (PFUser.currentUser() == nil) {
var logInViewController = LoginViewController()
logInViewController.delegate = self
var signUpViewController = SignUpViewController()
signUpViewController.delegate = self
logInViewController.signUpController = signUpViewController
self.presentViewController(logInViewController, animated: true, completion: nil)
}
}
func logInViewController(logInController: PFLogInViewController,
shouldBeginLogInWithUsername username: String, password: String) -> Bool {
if (!username.isEmpty || !password.isEmpty) {
return true
} else {
let alertController = UIAlertController(title: "Failed to login.",
message: "Login Failure. Please try again.", preferredStyle: .Alert)
let defaultAction = UIAlertAction(title: "OK", style: .Default, handler: nil)
alertController.addAction(defaultAction)
self.presentViewController(alertController, animated: true, completion: nil)
return false
}
}
func logInViewController(logInController: PFLogInViewController,
didFailToLogInWithError error: NSError?) {
println("Failed to log in.")
}
func signUpViewController(signUpController: PFSignUpViewController,
shouldBeginSignUp info: [NSObject : AnyObject]) -> Bool {
if let password = info["password"] as? String {
return count(password.utf16) >= 8
} else {
return false
}
}
func signUpViewController(signUpController: PFSignUpViewController,
didFailToSignUpWithError error: NSError?) {
println("Failed to sign up.")
}
func logInViewController(logInController: PFLogInViewController,
didLogInUser user: PFUser) {
let installation = PFInstallation.currentInstallation()
installation["user"] = PFUser.currentUser()
installation.saveInBackground()
self.dismissViewControllerAnimated(true, completion: nil)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func signUpViewControllerDidCancelSignUp(signUpController:
PFSignUpViewController) {
println("User dismissed signup.")
}
}
After reading another user's answer which seemed like it would be the answer, I added the following class to my workspace:
import Foundation
class AlertHelper: NSObject {
func showAlert(fromController controller: MasterViewController) {
var alert = UIAlertController(title: "abc", message: "def", preferredStyle: .Alert)
controller.presentViewController(alert, animated: true, completion: nil)
}
}
And then modified my method accordingly:
func logInViewController(logInController: PFLogInViewController,
shouldBeginLogInWithUsername username: String, password: String) -> Bool {
if (!username.isEmpty || !password.isEmpty) {
return true
} else {
let alertController = UIAlertController(title: "Failed to login.",
message: "Login Failure. Please try again.", preferredStyle: .Alert)
let defaultAction = UIAlertAction(title: "OK", style: .Default, handler: nil)
alertController.addAction(defaultAction)
var alert = AlertHelper()
alert.showAlert(fromController: self)
return false
}
}
At this point I'm not quite sure what else to do. One thought I had was to programmatically add a UILabel to my LoginViewController and SignUpViewController and change the content based on the errors (or lack thereof) for the user login and signup, but I would like to have alerts.
EDIT: This is the code in my LoginViewController. It subclassed in order to customize the appearance. The code for my SignUpViewController is almost identical.
import UIKit
import Parse
import ParseUI
class LoginViewController: PFLogInViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = ckPurple
let label = UILabel()
label.textColor = UIColor.whiteColor()
label.text = "Welcome."
label.sizeToFit()
logInView?.logo = label
// Do any additional setup after loading the view.
}
The problem is that you present a login view controller from your master view controller, which removes the master view controller from the view hierarchy. You then attempt to present an alert view controller from your master view controller, but you need to present from a view controller in the view hierarchy. Try presenting the alert from your login view controller instead.
loginController.presentViewController(alertController, animated: true, completion: nil)

Resources