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
Related
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
}
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.
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)
}
}
I have followed the sample here
https://github.com/awslabs/aws-sdk-ios-samples/tree/master/CognitoYourUserPools-Sample
To integrate interactive cognito login to my iOS app. This is all working well, but when a new user is created in the pool, they initially have a FORCE_CHANGE_PASSWORD status.
For android you can follow the procedure below
http://docs.aws.amazon.com/cognito/latest/developerguide/using-amazon-cognito-user-identity-pools-android-sdk-authenticate-admin-created-user.html
But for iOS I can't find out how to do this. Using the sample, if I attempt to login with a user in FORCE_CHANGE_PASSWORD status, I see the following output in the console logs (with some values removed for brevity):
{"ChallengeName":"NEW_PASSWORD_REQUIRED","ChallengeParameters":{"requiredAttributes":"[]","userAttributes":"{\"email_verified\":\"true\",\"custom:autoconfirm\":\"Y\","Session":"xyz"}
The following is the code from the SignInViewController from the sample detailed above.
import Foundation
import AWSCognitoIdentityProvider
class SignInViewController: UIViewController {
#IBOutlet weak var username: UITextField!
#IBOutlet weak var password: UITextField!
var passwordAuthenticationCompletion: AWSTaskCompletionSource<AWSCognitoIdentityPasswordAuthenticationDetails>?
var usernameText: String?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.password.text = nil
self.username.text = usernameText
self.navigationController?.setNavigationBarHidden(true, animated: false)
}
#IBAction func signInPressed(_ sender: AnyObject) {
if (self.username.text != nil && self.password.text != nil) {
let authDetails = AWSCognitoIdentityPasswordAuthenticationDetails(username: self.username.text!, password: self.password.text! )
self.passwordAuthenticationCompletion?.set(result: authDetails)
} else {
let alertController = UIAlertController(title: "Missing information",
message: "Please enter a valid user name and password",
preferredStyle: .alert)
let retryAction = UIAlertAction(title: "Retry", style: .default, handler: nil)
alertController.addAction(retryAction)
}
}
}
extension SignInViewController: AWSCognitoIdentityPasswordAuthentication {
public func getDetails(_ authenticationInput: AWSCognitoIdentityPasswordAuthenticationInput, passwordAuthenticationCompletionSource: AWSTaskCompletionSource<AWSCognitoIdentityPasswordAuthenticationDetails>) {
self.passwordAuthenticationCompletion = passwordAuthenticationCompletionSource
DispatchQueue.main.async {
if (self.usernameText == nil) {
self.usernameText = authenticationInput.lastKnownUsername
}
}
}
public func didCompleteStepWithError(_ error: Error?) {
DispatchQueue.main.async {
if let error = error as? NSError {
let alertController = UIAlertController(title: error.userInfo["__type"] as? String,
message: error.userInfo["message"] as? String,
preferredStyle: .alert)
let retryAction = UIAlertAction(title: "Retry", style: .default, handler: nil)
alertController.addAction(retryAction)
self.present(alertController, animated: true, completion: nil)
} else {
self.username.text = nil
self.dismiss(animated: true, completion: nil)
}
}
}
}
When didCompleteStepWithError executes, error is null where I would expect it to indicate something to tell us that the user is required to change password.
My question is really how to catch the "ChallengeName":"NEW_PASSWORD_REQUIRED" json that is output to the console?
Sorted this now. Posting it here in case it helps anyone else.
First, you need to create a view controller that will allow the user to specify the new password. The ViewController should have a AWSTaskCompletionSource<AWSCognitoIdentityNewPasswordRequiredDetails> as a property:
var newPasswordCompletion: AWSTaskCompletionSource<AWSCognitoIdentityNewPasswordRequiredDetails>?
Following the pattern in the sample referred to in the question, I then implemented AWSCognitoIdentityNewPasswordRequired as an extension to the view controller as follows:
extension NewPasswordRequiredViewController: AWSCognitoIdentityNewPasswordRequired {
func getNewPasswordDetails(_ newPasswordRequiredInput: AWSCognitoIdentityNewPasswordRequiredInput, newPasswordRequiredCompletionSource:
AWSTaskCompletionSource<AWSCognitoIdentityNewPasswordRequiredDetails>) {
self.newPasswordCompletion = newPasswordRequiredCompletionSource
}
func didCompleteNewPasswordStepWithError(_ error: Error?) {
if let error = error as? NSError {
// Handle error
} else {
// Handle success, in my case simply dismiss the view controller
self.dismiss(animated: true, completion: nil)
}
}
}
Again following the design of the sample detailed in the question, add a function to the AWSCognitoIdentityInteractiveAuthenticationDelegate extension to AppDelegate:
extension AppDelegate: AWSCognitoIdentityInteractiveAuthenticationDelegate {
func startPasswordAuthentication() -> AWSCognitoIdentityPasswordAuthentication {
//Existing implementation
}
func startNewPasswordRequired() -> AWSCognitoIdentityNewPasswordRequired {
// Your code to handle how the NewPasswordRequiredViewController is created / presented, the view controller should be returned
return self.newPasswordRequiredViewController!
}
}
The perfect example to implement the ResetPassword when u create the user in Cognito Console.
But, Integrating this ResetPassword mechanism to your existing app is your responsibility.
https://github.com/davidtucker/CognitoSampleApplication/tree/article1/CognitoApplication
I have a Protocol
import UIKit
import Alamofire
import SwiftyJSON
protocol RequestProtocol: class {
func RequestConnection(json: JSON, status: Int, Message: String)
}
class API: UIViewController {
var delegate: RequestProtocol?
var json: JSON = []
override func viewDidLoad() {
super.viewDidLoad()
}
func RequestConnection() {
Alamofire.request(variablesClass.url).responseJSON { (response) -> Void in
switch response.result {
case .success:
let result = response.result.value
if response.response?.statusCode == 200 {
self.json = JSON(result!)
DispatchQueue.main.async(execute: {
self.delegate?.RequestConnection(json: self.json, status: (response.response?.statusCode)!, Message: "\(self.json["Message"])")
})
} else {
self.json = JSON(result!)
DispatchQueue.main.async(execute: {
self.delegate?.RequestConnection(json: self.json, status: (response.response?.statusCode)!, Message: "\(self.json["Message"])")
})
}
break;
case .failure(let error):
print(error)
break;
}
}
}
}
I have it called in my Main View
class ViewController: UIViewController, RequestProtocol {
func RequestConnection(json: JSON, status: Int, Message: String) {
func showAlertView()
}
override func viewDidLoad() {
super.viewDidLoad()
var senderDelegateRequest = API()
var receiverDelegateViewController = ViewController()
senderDelegateRequest.delegate = receiverDelegateRequestRegistro
senderDelegateRequest.RequestConnection()
}
func showAlertView(){
let alertView = UIAlertController(title: "You need to log in first", message: "To access the special features of the app you need to log in first.", preferredStyle: .alert)
alertView.addAction(UIAlertAction(title: "Login", style: .default, handler: { (alertAction) -> Void in
}))
alertView.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
present(alertView, animated: true, completion: nil)
}
Why the Protocol does'nt execute the function to display the alert.
There is some way that as soon as the protocol is executed it can execute some function
In your ViewController class you are conforming to the protocol RequestProtocol. This means you can be the delegate of API. You created an instance called receiverDelegateViewController but this instance isn't loaded yet. You should change:
senderDelegateRequest.delegate = receiverDelegateRequestRegistro
to
senderDelegateRequest.delegate = self
And the delegate in API should call RequestConnection, not your ViewController.