Swift Extension with delegation (UIViewController) - ios

I need ability to send email in a number of view controllers in my app. The code is same, take three params -- recipient address, body, and subject. If Mail is configured on device, initialize MFMailComposeViewController with the view controller as delegate. If Mail is not configured, throw an error. Also set current view controller as mailComposeDelegate to listen to callbacks. How does one use Swift extension to achieve it (setting delegate in extension being the main issue)?

I think you should create service Class for this type of Problem so it can be reused in other Application.
class MailSender : NSObject , MFMailComposeViewControllerDelegate {
var currentController : UIViewController!
var recipient : [String]!
var message : String!
var compltion : ((String)->())?
init(from Controller:UIViewController,recipint:[String],message:String) {
currentController = Controller
self.recipient = recipint
self.message = message
}
func sendMail() {
if MFMailComposeViewController.canSendMail() {
let mail = MFMailComposeViewController()
mail.mailComposeDelegate = self
mail.setToRecipients(recipient)
mail.setMessageBody(message, isHTML: true)
currentController.present(mail, animated: true)
} else {
if compltion != nil {
compltion!("error")
}
}
}
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
if compltion != nil {
compltion!("error")
}
controller.dismiss(animated: true)
}
}
now you can send mail From All three Controller using Following Code.
let mailsender = MailSender(from: self,recipint:["example#via.com"],message:"your message")
mailsender.sendMail()
mailsender.compltion = { [weak self] result in
print(result)
//other stuff
}
remember I have used simple Clouser(completion) that take String as Argument to inform whether it is success or fails but you can write as per your requirment.in addition you can also use delegate pattern instead of clouser or callback.
main advantage of this type of service Class is dependancy injection.for more details : https://medium.com/#JoyceMatos/dependency-injection-in-swift-87c748a167be

Create a global function:
func sendEmail(address: String, body: String, subject: String, viewController: UIViewController) {
//check if email is configured or throw error...
}

Related

How can we write Unit Test Case for the function in swift ios

I want to write a unit test case for the method which is one of the delegate methods in the view controller. I created a unit test case class for the VC and am trying to write a unit test for the method.
Here is the method which is implemented in VC. How can we write Unit Test Case?
extension DownloadBaseViewController:EMPDecisionTreeCoordinatorDelegate {
func decisionEmptyTreeFeedbackButtonTapped() {
if let feedbackNavVc = storyboard?.instantiateViewController(identifier: "PremiumFeedbackNavViewController") as? PremiumCustomNavigationController {
if let feedbackVc = feedbackNavVc.children.first as? PremiumFeedbackViewController {
feedbackVc.id = self.fileDetails?.id
self.decesiontreeCoordinator!.rootViewController.present(feedbackNavVc, animated: true, completion: nil)
}
}
}
}
Created a unit test class for VC and tried not able to write it properly followed few tutorials not found for delegate method.
import XCTest
class DownloadBaseViewControllerTests: XCTestCase {
var downloadBaseViewController: DownloadBaseViewController!
func testDecisionEmptyTreeFeedbackButtonTapped() throws {
let feedbackVCNavigation = downloadBaseViewController.decisionEmptyTreeFeedbackButtonTapped
XCTAssertNotNil(feedbackVCNavigation, "Download base view controller contains feedback view controller and succesfully able to navigate")
///Test case Build succeded but this is not the way to test it properly need heads up on this.
}
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
}
}
Refactor the DownloadBaseViewController in your app so you can mock the dependency:
extension DownloadBaseViewController:EMPDecisionTreeCoordinatorDelegate {
// Add this variable in DownloadBaseViewController
lazy var presentingController: ViewControllerPresenting? = self.decesiontreeCoordinator?.rootViewController
func decisionEmptyTreeFeedbackButtonTapped() {
if let feedbackNavVc = storyboard?.instantiateViewController(identifier: "PremiumFeedbackNavViewController") as? PremiumCustomNavigationController {
if let feedbackVc = feedbackNavVc.children.first as? PremiumFeedbackViewController {
feedbackVc.id = self.fileDetails?.id
self.presentingController?.present(feedbackNavVc, animated: true, completion: nil)
}
}
}
}
// You need this to mock the foreign dependency on UIViewController
protocol ViewControllerPresenting: AnyObject {
func present(_ viewControllerToPresent: UIViewController,
animated flag: Bool,
completion: (() -> Void)?)
}
extension UIViewController: ViewControllerPresenting {}
In the tests you inject a Spy object that will help you validate the correct behaviour:
final class UIViewControllerSpy: ViewControllerPresenting {
var viewControllerToPresent: UIViewController!
func present(_ viewControllerToPresent: UIViewController,
animated flag: Bool,
completion: (() -> Void)? = nil) {
self.viewControllerToPresent = viewControllerToPresent
}
}
class DownloadBaseViewControllerTests: XCTestCase {
var downloadBaseViewController: DownloadBaseViewController! = DownloadBaseViewController()
func testDecisionEmptyTreeFeedbackButtonTapped() throws {
// Given
let spyController = UIViewControllerSpy()
downloadBaseViewController.presentingController = spyController
// When
downloadBaseViewController.decisionEmptyTreeFeedbackButtonTapped()
// Then
let presentedController = spyController.viewControllerToPresent as? PremiumFeedbackViewController
XCTAssertNotNil(presentedController, "Download base view controller contains feedback view controller and succesfully able to navigate")
}
}

In Cognito on iOS, handling new password required doesn't ever reach didCompleteNewPasswordStepWithError

I'm trying to implement functionality to respond to FORCE_CHANGE_PASSWORD on my iOS app that uses AWS Cognito. I used this Stack Overflow question which references this sample code. Right now, my code opens a view controller like it's supposed to; however, once on that view controller, I can't get it to do anything. In the sample code, it seems that when you want to submit the password change request you call .set on an instance of AWSTaskCompletionSource<AWSCognitoIdentityNewPasswordRequiredDetails>, yet when I do this, the protocol function didCompleteNewPasswordStepWithError is never called. Interestingly, the other protocol function getNewPasswordDetails is called quickly after viewDidLoad and I can't tell why. I believe this shouldn't be called until the user has entered their new password, etc and should be in response to .set but I could be wrong.
My code is pretty identical to the sample code and that SO post, so I'm not sure what's going wrong here.
My relevant AppDelegate code is here:
extension AppDelegate: AWSCognitoIdentityInteractiveAuthenticationDelegate {
func startNewPasswordRequired() -> AWSCognitoIdentityNewPasswordRequired {
//assume we are presenting from login vc cuz where else would we be presenting that from
DispatchQueue.main.async {
let presentVC = UIApplication.shared.keyWindow?.visibleViewController
TransitionHelperFunctions.presentResetPasswordViewController(viewController: presentVC!)
print(1)
}
var vcToReturn: ResetPasswordViewController?
returnVC { (vc) in
vcToReturn = vc
print(2)
}
print(3)
return vcToReturn!
}
//put this into its own func so we can call it on main thread
func returnVC(completion: #escaping (ResetPasswordViewController) -> () ) {
DispatchQueue.main.sync {
let storyboard = UIStoryboard(name: "ResetPassword", bundle: nil)
let resetVC = storyboard.instantiateViewController(withIdentifier: "ResetPasswordViewController") as? ResetPasswordViewController
completion(resetVC!)
}
}
}
My relevant ResetPasswordViewController code is here:
class ResetPasswordViewController: UIViewController, UITextFieldDelegate {
#IBAction func resetButtonPressed(_ sender: Any) {
var userAttributes: [String:String] = [:]
userAttributes["given_name"] = firstNameField.text!
userAttributes["family_name"] = lastNameField.text!
let details = AWSCognitoIdentityNewPasswordRequiredDetails(proposedPassword: self.passwordTextField.text!, userAttributes: userAttributes)
self.newPasswordCompletion?.set(result: details)
}
}
extension ResetPasswordViewController: AWSCognitoIdentityNewPasswordRequired {
func getNewPasswordDetails(_ newPasswordRequiredInput: AWSCognitoIdentityNewPasswordRequiredInput, newPasswordRequiredCompletionSource: AWSTaskCompletionSource<AWSCognitoIdentityNewPasswordRequiredDetails>) {
self.newPasswordCompletion = newPasswordRequiredCompletionSource
}
func didCompleteNewPasswordStepWithError(_ error: Error?) {
DispatchQueue.main.async {
if let error = error as? NSError {
print("error")
print(error)
} else {
// Handle success, in my case simply dismiss the view controller
SCLAlertViewHelperFunctions.displaySuccessAlertView(timeoutValue: 5.0, title: "Success", subTitle: "You can now login with your new passowrd", colorStyle: Constants.UIntColors.emeraldColor, colorTextButton: Constants.UIntColors.whiteColor)
self.dismiss(animated: true, completion: nil)
}
}
}
}
Thank you so much for your help in advance and let me know if you need any more information.

MFMailComposeViewController delegate not working on swift 4

I'm trying to dismiss the MFMailComposeViewController but the delegate is not triggered. It seems that it's a common issue and the answers are the same, but they do not work for me :(. I have a button that calls the function to send the mail. I first create a csv file, then the MFMailComposeViewController and attach the csv file to the mail. The mail is sent sometimes (the mail view controller does not dismiss after that) and the cancel button shows me the option to delete or save the draft but after that nothing happens.
Here's the code of the button:
import UIKit
import MessageUI
class UserInfoViewController: UIViewController, MFMailComposeViewControllerDelegate {
#IBAction func uploadPressed(_ sender: Any) {
let contentsOfFile = "Name,ID,Age,Sex,Time,\n\(name),\(id),\(age),\(sex),\(time)"
let data = contentsOfFile.data(using: String.Encoding.utf8, allowLossyConversion: false)
if let content = data {
print("NSData: \(content)")
}
func configuredMailComposeViewController() -> MFMailComposeViewController {
let emailController = MFMailComposeViewController()
//emailController.mailComposeDelegate = self as? MFMailComposeViewControllerDelegate
emailController.mailComposeDelegate = self
emailController.setToRecipients([""])
emailController.setSubject("CSV File")
emailController.setMessageBody("", isHTML: false)
emailController.addAttachmentData(data!, mimeType: "text/csv", fileName: "registro.csv")
return emailController
}
let emailViewController = configuredMailComposeViewController()
if MFMailComposeViewController.canSendMail() {
self.present(emailViewController, animated: true, completion: nil)
}
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
print("Delegate worked!")
controller.dismiss(animated: true, completion: nil)
}
}
}
Thank you very much in advance.
Your issue is being caused by putting the delegate method inside another method. You can't do that. Delegate functions need to be at the top level of the class. Simply move your mailComposeController(_:didFinishWith:error:) function out of the uploadPressed function.

Generic Paramter 'T' could not be inferred - Problems with generics

I have this class called openApp, thats meant to open another app using a redirect url and store kit. Im not too familiar with generics and its making me run into this error
Generic Parameter 'T' could not be inferred
Am I not handling the use of T correctly? I really don't understand whats going on here.
public class openApp {
static func openOrDownloadPlayPortal<T>(delegate: T) where T: SKStoreProductViewControllerDelegate, T:
UIViewController {
let storeProductVC = SKStoreProductViewController()
let playPortalURL = URL(string: "redirect url")!
if UIApplication.shared.canOpenURL(playPortalURL) {
UIApplication.shared.openURL(playPortalURL)
}
else {
let vc = SKStoreProductViewController()
let params = [
SKStoreProductParameterITunesItemIdentifier: "app identifier"
]
vc.loadProduct(withParameters: params) { success, err in
if let err = err {
}
}
vc.delegate = delegate
delegate.present(vc, animated: true, completion: nil)
}
}
}
Since the issue appears when calling the openOrDownloadPlayPortal method as:
openApp.openOrDownloadPlayPortal(delegate: self)
You will face the mentioned error:
Generic parameter 'T' could not be inferred
if your class does not conforms to the SKStoreProductViewControllerDelegate. For example, let's assume that you are calling it in ViewController class, as:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
openApp.openOrDownloadPlayPortal(delegate: self)
}
}
So, you have to make sure that ViewController has:
extension ViewController: SKStoreProductViewControllerDelegate {
// ...
}
The reason of the error is: the compiler assumes that the T parameter in openOrDownloadPlayPortal method has to conforms to the SKStoreProductViewControllerDelegate, therefore implementing
openApp.openOrDownloadPlayPortal(delegate: self)
means that it will not be recognized as the appropriate type for the T, unless you make self (ViewController in the above example) to be conformable to SKStoreProductViewControllerDelegate.

how to wait for ui delegate in swift

I want to create simple object helper to sending sms in many places on my Swift 2.0 app, and in next step another helpers (email, pdf opener etc)
I create simple class:
import Foundation
import MessageUI
class SmsHelper: MFMessageComposeViewControllerDelegate {
func sendSMS(body: String){
if (MFMessageComposeViewController.canSendText()){
let messageVC = MFMessageComposeViewController()
messageVC.body = body
//messageVC.recipients = ["Enter tel-nr"]
messageVC.messageComposeDelegate = self;
AppDelegate().sharedInstance().getTopController().presentViewController(messageVC, animated: false, completion: nil)
}
else{
//do some alert etc.
}
}
func messageComposeViewController(controller: MFMessageComposeViewController, didFinishWithResult result: MessageComposeResult){
sendSMSRsp(result, errorMsg: nil)
controller.dismissViewControllerAnimated(true, completion: nil)
print("sms didFinishWithResult")
}
}
in anywhere in code I want to do that:
class someAnotherClass{
func someFunction(){
let smsHelper = SmsHelper()
smsHelper.sendSms("some text")
}
}
so sms ios editor is opened, but when I want to close it or sending, it don't dismiss, function messageComposeViewController(controller: MFMessageComposeViewController, didFinishWithResult result: MessageComposeResult) i never called and app crashes with memory leak, I know reason: is because SmsHelper object in 'someFunction' is deleting after end scope of this function, and this object is nil, and system try to call didFinishWithResult at nil object. I confirm it: when I add smsHelper object as member of 'SomeClass' it worked - delegate is called.
Question is: What is the best practice to do that, adding a member is not an option for me, because many classes can use that, also creating a singleton, appDelegate member is I think quite stupid. How to force not deleting a object at end of scope function?
Try to use singleton way to do it:
class SmsHelper: MFMessageComposeViewControllerDelegate {
static let sharedInstance = SmsHelper()
private init() {}
}
And usage:
SmsHelper.sharedInstance().sendSms('some text')
EDIT:
If you add different init method from abstract class.
private override init(message: String){
super.init()
}
then change definition to static let sharedInstance = SmsHelper(message:"smsHelper") won't complicated.
For your delegate, just assign it out of the init method, in the UIViewController viewDidLoad,
SmsHelper.sharedInstance().delegate = self
For me, the way to do this is to create a type alis of a closure, then storing an optional variable of this type alias. When you call sendSMS, you pass in a closure and set it to your variable. Then that can be called in the delegate, which will then pass it through the closure of sendSMS:
import Foundation
import MessageUI
class SmsHelper: MFMessageComposeViewControllerDelegate {
let messageVC: MFMessageComposeViewController!
typealias SMSCompletion = (result: MessageComposeResult, errorMsg: NSError?) -> ()
var sendSMSRsp: SMSCompletion?
func sendSMS(body: String, completion: SMSCompletion){
sendSMSRsp = completion
if (MFMessageComposeViewController.canSendText()){
messageVC = MFMessageComposeViewController()
messageVC.body = body
//messageVC.recipients = ["Enter tel-nr"]
messageVC.messageComposeDelegate = self;
AppDelegate().sharedInstance().getTopController().presentViewController(messageVC, animated: false, completion: nil)
} else {
//do some alert etc.
}
}
func messageComposeViewController(controller: MFMessageComposeViewController, didFinishWithResult result: MessageComposeResult){
sendSMSRsp?(result: result, errorMsg: nil)
controller.dismissViewControllerAnimated(true, completion: nil)
print("sms didFinishWithResult")
}
}
class SomeOtherClass {
func someFunction() {
let smsHelper = SmsHelper()
smsHelper.sendSMS("some text") { (result, errorMsg) -> () in
}
}
}

Resources