I have a class thats used to help manage the process of users sending emails with MFMailComposeViewControllerDelegate.
The code is rather long, and I'm using it inside of almost all of my ViewControllers. And I figure I should be able to add it as an extension to UIVIewController. And so I'm currently trying to do that, but at a loss of how to do it correctly.
Working Code:
Struct:
struct Feedback {
let recipients = [R.App.Contact.email] // [String]
let subject: String
let body: String
let footer: String
}
Class:
final class FeedbackManager: NSObject, MFMailComposeViewControllerDelegate {
private var feedback: Feedback
private var completion: ((Result<MFMailComposeResult,Error>)->Void)?
override init() {
fatalError("Use FeedbackManager(feedback:)")
}
init?(feedback: Feedback) {
print("init()")
guard MFMailComposeViewController.canSendMail() else {
return nil
}
self.feedback = feedback
}
func send(on viewController: UIViewController, completion:(#escaping(Result<MFMailComposeResult,Error>)->Void)) {
print("send()")
var appVersion = ""
if let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String {
appVersion = version
}
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)<br>Potfolio: (\(appVersion))<br>\(UIDevice.modelName) (\(UIDevice.current.systemVersion))</p>", isHTML: true)
DispatchQueue.main.async {
viewController.present(mailVC, animated:true)
}
}
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
print("mailComposeController()")
if let error = error {
completion?(.failure(error))
controller.dismiss(animated: true)
} else {
completion?(.success(result))
controller.dismiss(animated: true)
}
}
}
Implementation:
var feedbackManager: FeedbackManager?
func sendEmail() {
let feedback = Feedback(subject: "subject", body: "body", footer: "footer")
if let manager = FeedbackManager(feedback: feedback) {
self.feedbackManager = manager
self.feedbackManager?.send(on: self) { [weak self] result in
switch result {
case .failure(let error):
print("error: ", error.localizedDescription)
case .success(_):
print("Success")
}
self?.feedbackManager = nil
}
} else { // Cant Send Email:
let failedMenu = UIAlertController(title: "Please email " + R.App.Contact.email, message: nil, preferredStyle: .alert)
let okAlert = UIAlertAction(title: "Ok!", style: .default)
failedMenu.addAction(okAlert)
DispatchQueue.main.async {
self.present(failedMenu, animated: true)
}
}
}
My attempt to refractor:
import UIKit
extension UIViewController {
func refactoredEmail(on viewController: UIViewController, with feedback: Feedback, manager: inout FeedbackManager?) -> FeedbackManager? {
guard let letManager = FeedbackManager(feedback: feedback) else {
let failedMenu = UIAlertController(title: "Please email " + R.App.Contact.email, message: nil, preferredStyle: .alert)
let okAlert = UIAlertAction(title: "Ok!", style: .default)
failedMenu.addAction(okAlert)
DispatchQueue.main.async {
viewController.present(failedMenu, animated: true)
}
return nil
}
manager = letManager
manager?.send(on: self) { [weak manager] result in
switch result {
case .failure(let error):
print("error: ", error.localizedDescription)
case .success(_):
print("Success")
}
manager = nil
}
return letManager
}
}
Implementation:
var feedbackManager: FeedbackManager?
func sendEmail() {
let feedback = Feedback(subject: "subject", body: "body", footer: "footer")
refactoredEmail(on: self, with: feedback, manager: &feedbackManager)
}
FeedbackManager Class is the same as above. --
As is, this is functional, but, refactoredEmail uses feedbackManager as an inout parameter. And feedbackManager is not returned to nil after completion.
I'm not entirely sure how bad (or not) it is to use inout. And I don't fully understand closures. However, my understanding is that the FeedbackManager Class must be a class because it subclasses MFMailComposeViewControllerDelegate. And because of this, each ViewController needs it own reference to the class. Which is where var feedbackManager: FeedbackManager? comes in.
feedbackManager can then run the code inside the FeedbackManager Class. Which presents the MFMailComposeViewController on top of the ViewController that feedbackManager is in. But since now refactoredEmail() is an extension of UIViewController, my guess is the closure is capturing incorrectly?
In conclusion: My goal is to refractor the code so that implementing it in each view controller can hopefully be cut down in comparison with the first implementation code block.
Also, how bad is it to use inout?
I'm not sure why manager is not nil after completion, but I can show you other way of implementation.
You could use associated objects:
protocol FeedbackShowable: class {
func giveFeedback(with feedback: Feedback)
}
extension UIViewController: FeedbackShowable {
private enum AssociatedObjectKeys {
static var feedbackManager: String = "feedbackManager"
}
private var feedbackManager: FeedbackManager? {
get {
return objc_getAssociatedObject(self, &AssociatedObjectKeys.feedbackManager) as? FeedbackManager
}
set {
objc_setAssociatedObject(self,
&AssociatedObjectKeys.feedbackManager,
newValue as FeedbackManager?,
.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
func giveFeedback(with feedback: Feedback) {
let feedbackManager = FeedbackManager(feedback: feedback)
self.feedbackManager = feedbackManager
feedbackManager.send(on: self) { [weak self] result in
self?.feedbackManager = nil
}
}
}
Related
There is UIAlertController with text field in my View Controller. When user enter name of the city, this data must be transmitted to Model, when I get coordinates of this city. But I can't to pass name of the city from View Controller to Model
My UIAlertController:
class MainScrenenViewController: UIViewController {
var delegate: ILocationGroup?
#objc func locationButtonTap() {
let alert = UIAlertController(title: "Add city", message: nil, preferredStyle: .alert)
let addButton = UIAlertAction(title: "Add", style: .default) { action in
self.delegate?.addLocation(alert.textFields?.first?.text ?? "No City")
}
alert.addAction(addButton)
let cancelButton = UIAlertAction(title: "Cancel", style: .default, handler: nil)
alert.addAction(cancelButton)
alert.addTextField { textField in
textField.placeholder = "Your City"
}
present(alert, animated: true, completion: nil)
}
My Model:
protocol ILocationGroup {
func addLocation(_ name: String)
}
class LocationGroup: ILocationGroup {
var mainScreenViewController: MainScrenenViewController?
func addLocation(_ name: String) {
mainScreenViewController?.delegate = self
let url = "https://geocode-maps.yandex.ru/1.x/?apikey=fd93783b-fe25-4428-8c3b-38b155941c8c&format=json&geocode=\(name)"
guard let url = URL(string: url) else { return }
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data, error == nil else { return }
do {
let result = try JSONDecoder().decode(LocationData.self, from: data)
print(result.response.geoObjectCollection.metaDataProperty.geocoderResponseMetaData.boundedBy.envelope.lowerCorner)
}
catch {
print("failed to convert \(error)")
}
}
task.resume()
}
}
I think it is supposed to be var delegate: LocationGroup()
Also, I wouldn't be calling it delegate because registered delegate is a keyword in swift
https://manasaprema04.medium.com/different-ways-to-pass-data-between-viewcontrollers-views-8b7095e9b1bf
I have a Swift file that gets details about the user that is currently logged in/signed up named CognitoUserPoolController.swift
import Foundation
import AWSCognitoIdentityProvider
class CognitoUserPoolController {
let userPoolRegion: AWSRegionType = "Private Info"
let userPoolID = "Private Info"
let appClientID = "Private Info"
let appClientSecret = "Private Info"
var userPool:AWSCognitoIdentityUserPool?
var currentUser:AWSCognitoIdentityUser? {
get {
return userPool?.currentUser()
}
}
static let sharedInstance: CognitoUserPoolController = CognitoUserPoolController()
private init() {
let serviceConfiguration = AWSServiceConfiguration(region: userPoolRegion, credentialsProvider: nil)
let poolConfiguration = AWSCognitoIdentityUserPoolConfiguration(clientId: appClientID,
clientSecret: appClientSecret,
poolId: userPoolID)
AWSCognitoIdentityUserPool.register(with: serviceConfiguration,
userPoolConfiguration: poolConfiguration,
forKey:"AWSChat")
userPool = AWSCognitoIdentityUserPool(forKey: "AWSChat")
AWSDDLog.sharedInstance.logLevel = .verbose
}
func login(username: String, password:String, completion:#escaping (Error?)->Void) {
let user = self.userPool?.getUser(username)
let task = user?.getSession(username, password: password, validationData:nil)
task?.continueWith(block: { (task: AWSTask<AWSCognitoIdentityUserSession>) -> Any? in
if let error = task.error {
completion(error)
return nil
}
completion(nil)
return nil
})
}
func signup(username: String, password:String, emailAddress:String, completion:#escaping (Error?, AWSCognitoIdentityUser?)->Void) {
var attributes = [AWSCognitoIdentityUserAttributeType]()
let emailAttribute = AWSCognitoIdentityUserAttributeType(name: "email", value: emailAddress)
attributes.append(emailAttribute)
print(emailAttribute.value!)
let task = self.userPool?.signUp(username, password: password, userAttributes: attributes, validationData: nil)
task?.continueWith(block: {(task: AWSTask<AWSCognitoIdentityUserPoolSignUpResponse>) -> Any? in
if let error = task.error {
completion(error, nil)
return nil
}
guard let result = task.result else {
let error = NSError(domain: "Private Info",
code: 100,
userInfo: ["__type":"Unknown Error", "message":"Cognito user pool error."])
completion(error, nil)
return nil
}
completion(nil, result.user)
return nil
})
}
func confirmSignup(user: AWSCognitoIdentityUser, confirmationCode:String, completion:#escaping (Error?)->Void) {
let task = user.confirmSignUp(confirmationCode)
task.continueWith { (task: AWSTask<AWSCognitoIdentityUserConfirmSignUpResponse>) -> Any? in
if let error = task.error {
completion(error)
return nil
}
completion(nil)
return nil
}
}
func resendConfirmationCode(user: AWSCognitoIdentityUser, completion:#escaping (Error?)->Void) {
let task = user.resendConfirmationCode()
task.continueWith { (task: AWSTask<AWSCognitoIdentityUserResendConfirmationCodeResponse>) -> Any? in
if let error = task.error {
completion(error)
return nil
}
completion(nil)
return nil
}
}
func getUserDetails(user: AWSCognitoIdentityUser, completion:#escaping (Error?, AWSCognitoIdentityUserGetDetailsResponse?)->Void) {
let task = user.getDetails()
task.continueWith(block: { (task: AWSTask<AWSCognitoIdentityUserGetDetailsResponse>) -> Any? in
if let error = task.error {
completion(error, nil)
return nil
}
guard let result = task.result else {
let error = NSError(domain: "Private Info",
code: 100,
userInfo: ["__type":"Unknown Error", "message":"Cognito user pool error."])
completion(error, nil)
return nil
}
completion(nil, result)
return nil
})
}
}
After a user successfully signs up they are presented with HomeViewController. In HomeViewController I try to print an attribute email value like this but it does not work
import UIKit
import AWSCognitoIdentityProvider
class HomeViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let userpoolController = CognitoUserPoolController.sharedInstance
userpoolController.getUserDetails(user: userpoolController.currentUser!) { (error: Error?, details:AWSCognitoIdentityUserGetDetailsResponse?) in
view.backgroundColor = .green // This line of code works, but below this line it does not.
if let loggedInUserAttributes = details?.userAttributes {
self.view.backgroundColor = .systemPink
for attribute in loggedInUserAttributes {
if attribute.name?.compare("email") == .orderedSame {
print ("Email address of logged-in user is \(attribute.value!)")
}
}
}
}
}
}
The background color successfully changes to green but does not change to pink (That was to see if the code was working.) Inside of the if let statement is where the code is not working and there are not any errors. How can I properly fix this?
I'm trying to allow a user to report a user (in a Tinder-like app). To report, this button takes the user to a new VC to elaborate the issue as an email.
What I'm missing:
How can I add the reporter's and reportee's Firebase unique ID to the email (or whatever form of communication)? (So then I can investigate and take action as needed)
Here's what I have:
The code to properly send an email...
func configureMailController() -> MFMailComposeViewController {
let mailComposerVC = MFMailComposeViewController()
mailComposerVC.mailComposeDelegate = self
mailComposerVC.setToRecipients(["RadiusAppHelp#gmail.com"])
mailComposerVC.setSubject("Reporting user")
mailComposerVC.setMessageBody("Please include as much detail as possible:", isHTML: false)
return mailComposerVC
}
func showMailError() {
let sendMailErrorAlert = showAlert(withTitle: "Could not send message", message: "Please try again")
let dismiss = UIAlertAction(title: "Okay", 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)
}
}
The code to pull the other user's ID in the swipe view VC...
var otherUsersId = ""
var currentlyViewedUserId: String?
firebaseServer.fetchUsers {[weak self] (usersDict) in
self?.usersDict = usersDict
let fetchedUsers = Array(usersDict.values)
self?.filterBlockedUsers(from: fetchedUsers)
self?.loadFirstUser()
self?.cardView.reloadData()
}
func loadFirstUser() {
if users.count > 0 {
let imageView = UIImageView()
let storage = Storage.storage()
let storageRef = storage.reference(withPath:
"\(users[0].userId!)/photos/\(0)")
currentlyViewedUserId = users[0].userId
PhotoUploader.downloadImageUrl(from:
storageRef) { (url) in
guard let url = url else { return }
imageView.downloaded(from: url,
contentMode: .scaleAspectFill)
}
nameLbl.text = users[0].firstName
setupDetailsFor(user: users[0])
infoCollectionView.reloadData()
}
}
As well as the code to block a user (but blocking / reporting functions work independently).
Any help is greatly appreciated!
Okay, I found at least a temporary fix...
(Not using Firestore however, which I'll eventually need to implement — https://www.youtube.com/watch?v=Ofux_4c94FI)
In FirebaseFunctions.swift...
// Report Someone
func reportSomeone(with userId: String, completion:
#escaping (Error?) -> Void) {
let usersRef = db.child("users")
if let uid = Auth.auth().currentUser?.uid {
usersRef.child("\(uid)/report/\ .
(userId)").setValue(true) { (error, dbref) in
completion(error)
}
}
}
// Set Preferences for Reporting
func reportPreferences(with userId: String,
completion: #escaping (Error?) -> Void) {
let usersRef = db.child("users")
if let uid = Auth.auth().currentUser?.uid {
usersRef.child("\(uid)/preferences/\ .
(userId)").setValue(true) { (error, dbref) in
completion(error)
}
}
}
In User.swift...
var report: [String: Bool]? = [:]
func makeDictionary() -> [String: Any] {
print("")
return [
"report": report ?? [:]
]
static func makeObjectFrom(_ dictionary: [String:
Any]) -> LocalUser {
let report = dictionary["report"] as?
[String:Bool] ?? [:]
}
let localUser = LocalUser(report: report)
In the View Controller...
import Firebase
var currentUserId = Auth.auth().currentUser?.uid
var otherUsersId = ""
// Report Button
var isCurrentUserReported = false
var isOtherUserReported = false
var currentlyViewedUserId: String?
// FILTER REPORTED USERS
func filterReportUsers(from users: [LocalUser]) {
var notReportUsers = users
var notReportUsersDict = self.usersDict
var reportUsers = newUser.report ?? [:]
if let currentUserId =
Auth.auth().currentUser?.uid {
reportUsers[currentUserId] = true
}
for (userId, report) in reportUsers ?? [:] {
print("UserId...", userId)
print("UserHere...", usersDict[userId])
if let user = usersDict[userId] {
notReportUsersDict.removeValue(forKey:
userId)
}
}
let notReport = Array(notReportUsersDict.values)
self.users = notReport
}
This isn't perfect, but it works!
I am currently implementing a signup feature into a chat application I am working on. In my 'SignupViewController' I want to implement a function named 'signupButtonPressed' which routes me from a signup viewcontroller to a 'ListContacts' viewcontroller. If the signup fails, then a function called 'showValidationError' will execute. Code excerpt from my SignupViewController below:
#IBAction func signupButtonPressed(_ sender: Any) {
let request = Signup.Request(
name: fullNameTextField.text!,
email: emailTextField.text!,
password: passwordTextField.text!
)
interactor?.createAccount(request: request)
}
func showValidationError(_ message: String) {
let alertCtrl = UIAlertController(title: "Oops! An error occurred", message: message, preferredStyle: .alert)
alertCtrl.addAction(UIAlertAction(title: "Ok", style: .cancel, handler: nil))
self.show(alertCtrl, sender: self)
}
I am using Swift Clean Architecture, so I will link the code to my Signup Router, Model, and Interactor files also:
1) Signupinteractor.swift:
import Foundation
protocol SignupBusinessLogic {
func createAccount(request: Signup.Request)
}
class SignupInteractor: SignupBusinessLogic {
var viewController: SignupFormErrorLogic?
var router: (NSObjectProtocol & SignupRoutingLogic)?
var worker = UsersWorker()
func createAccount(request: Signup.Request) -> Void {
self.worker.signup(request: request) { user, error in
guard error == nil else {
print(error!)
self.viewController?.showValidationError("Error creating account!")
return
}
self.router?.routeToListContacts()
}
}
}
2) SignupModels.swift:
import Foundation
enum Signup {
struct Request {
var name: String
var email: String
var password: String
}
struct Response {
var user: User?
init(data: [String:Any]) {
self.user = User(
id: data["id"] as! Int,
name: data["name"] as! String,
email: data["email"] as! String,
chatkit_id: data["chatkit_id"] as! String
)
}
}
}
3) SignupRouter.swift:
import UIKit
#objc protocol SignupRoutingLogic {
func routeToListContacts()
}
class SignupRouter: NSObject, SignupRoutingLogic {
weak var viewController: SignupViewController?
func routeToListContacts() {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let destinationVC = storyboard.instantiateViewController(withIdentifier: "MainNavigator") as! UINavigationController
viewController!.show(destinationVC, sender: nil)
}
}
The signup function in my UsersWorker.swift file:
func signup(request: Signup.Request, completionHandler: #escaping (User?, UsersStoreError?) -> Void) {
let params: Parameters = [
"name": request.name,
"email": request.email,
"password": request.password
]
postRequest("/api/users/signup", params: params, headers: nil) { data in
guard data != nil else {
return completionHandler(nil, UsersStoreError.CannotSignup)
}
let response = Signup.Response(data: data!)
CurrentUserIDDataStore().setID(CurrentUserID(id: response.user?.chatkit_id))
let request = Login.Account.Request(
email: request.email,
password: request.password
)
self.login(request: request) { token, error in
guard error == nil else {
return completionHandler(nil, UsersStoreError.CannotLogin)
}
DispatchQueue.main.async {
completionHandler(response.user, nil)
}
}
}
}
When I enter signup details into the signup UITextfields (fullNameTextField; emailTextField; passwordTextField) and press the signup button, an error called 'CannotSignup' triggers. Unsure why however. This case can also be found in my UsersWorker.swift file:
enum UsersStoreError: Error {
case CannotLogin
case CannotSignup
case CannotFetchChatkitToken
}
Would be great if anyone is able to look over the code to get an idea for what the issue might be, and how I might resolve it? If any further info is required just ask!
Most probably the API call is failing, Please make a check on return HTTP status code instead of data. In some cases the API call can be success without any response data
Ideally send an Error instance as well along with data back from postRequest method
I got a getJSON Function with url parameter:
func getJsons(jsonUrl: String) {
guard let url = URL(string: jsonUrl) else { return }
URLSession.shared.dataTask(with: url:) { (data, response, err) in
if err != nil {
print("ERROR: \(err!.localizedDescription)")
let alertController = UIAlertController(title: "Error", message:
err!.localizedDescription, preferredStyle: UIAlertControllerStyle.alert)
alertController.addAction(UIAlertAction(title: "Dismiss", style: UIAlertActionStyle.default,handler: nil))
var topController:UIViewController = UIApplication.shared.keyWindow!.rootViewController!
while ((topController.presentedViewController) != nil) {
topController = topController.presentedViewController!;
}
topController.present(alertController, animated: true, completion: nil)
}
guard let data = data else { return }
do {
let test = try JSONDecoder().decode([ArticleStruct].self, from: data)
DispatchQueue.main.async {
self.myArticles = test
print(self.myArticles?.count ?? 0 )
self.myTableView.reloadData()
}
} catch let jsonErr {
print("Error:", jsonErr)
}
}.resume()
}
now i want to move the function to another class (network class).
what must I do to add a completionHandler to the function and how do I call it from other classes.
i want to return the json to the caller class.
My plan:
in MainActivity -> viewDidLoad: call network completionHandler(getJsons(192.168.178.100/getPicture.php))
on completion -> myJsonDataMainActivity = (json data from completionHandler)
-> MainActivity.TableView.reload
in otherClass -> call network completionHandler(getJsons(192.168.178.100/getData.php))
on completion -> myJsonDataOtherClass = (json data from completionHandler)
-> otherClass.TableView.reload
Thanks for your help!
You can use delegate.
myJsonDataOtherClass:
protocol NetworkDelegate {
func didFinish(result: Data)
}
class myJsonDataOtherClass {
var delegate: NetworkDelegate? = nil
...
func getJsons(jsonUrl: String) {
...
URLSession.shared.dataTask(with: url:) { (data, response, err) in
...
delegate?.didFinish(data)
}.resume()
}
}
and set delegate at MainActivity
class MainActivity: UIViewController, NetworkDelegate{
...
let jsonClass = myJsonDataOtherClass()
jsonClass.delegate = self
jsonClass.getJsons(jsonUrl:url)
func didFinish(result:Data) {
// process data
}
}
You should add a completion handler in your function and pass the JSON object.
func getJsons(jsonUrl: String, completion:#escaping (_ success: Bool,_ json: [String: Any]?) -> Void) {
...
completion(true, json)
}