CNContactViewController handle CNContactPhoneNumbersKey along with CallKit - ios

I have a VoIP application that use CallKit integration.
In my contacts screen I have a UITableView with all the device contacts and when the user press a contact I populate a CNContactViewController with:
extension ContactsViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
selectedContact = viewModel.contactAt(section: indexPath.section, index: indexPath.row)
Logd(self.logTag, "\(#function) \(String(describing: selectedContact))")
let contactViewController = CNContactViewController(for: selectedContact!)
contactViewController.title = CNContactFormatter.string(from: selectedContact!,
style: CNContactFormatterStyle.fullName)
contactViewController.contactStore = viewModel.contactStore
contactViewController.allowsActions = false
contactViewController.delegate = self
navigationItem.titleView = nil
navigationController?.pushViewController(contactViewController, animated: true)
tableView.deselectRow(at: indexPath, animated: false)
}
}
This populates the contacts details view without any problem.
I would like to catch the user action on the phone number pressing and perform the VoIP call so I use the following code:
extension ContactsViewController: CNContactViewControllerDelegate {
func contactViewController(_ viewController: CNContactViewController,
shouldPerformDefaultActionFor property: CNContactProperty) -> Bool {
if property.key == CNContactPhoneNumbersKey {
let phoneNumberProperty: CNPhoneNumber = property.value as! CNPhoneNumber
let phoneNumber = phoneNumberProperty.stringValue
makeMyVoIPCall(number: phoneNumber!, video: false)
//makeMyVoIPCall(number: "+1234567890", video: false)
return false
}
if property.key == CNContactSocialProfilesKey {
let profile: CNSocialProfile = property.value as! CNSocialProfile
if profile.service == appServiceName {
let phoneNumber = profile.username
makeMyVoIPCall(number: phoneNumber!, video: false)
return false
}
}
Logd(self.logTag, "\(#function) nothing to handle for \(property)")
return true
}
func contactViewController(_ viewController: CNContactViewController, didCompleteWith contact: CNContact?) {
dismiss(animated: true, completion: nil)
}
}
This has as a result when I press the phone item element to initiate 2 calls! One call is performed from my application (VoIP) and the other from the system (SIM/GSM).
What I tried:
Added the above code for handling the CNContactSocialProfilesKey and at this case the call is performed as expected only once via my application.
Changed the makeMyVoIPCall to a specific number instead of the pressed (see above commented line). Again I see 2 calls the system calls the clicked property and my application the "+1234567890".
I also verified that the return value should be false and not true when you handle the action.
What is required in order to tell the system that I am handling the action, and the SIM/GSM call should not performed?
I am testing on iOS 12.1.1 (16C50).

Wrapping the makeMyVoIPCall(number: phoneNumber!, video: false) in a main thread bundle solved the problem.
I cannot really understand why iOS just ignore the false value otherwise.

Related

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.

Listing scheduled notifications in a table view controller

I am trying to list all the notifications a user has created and scheduled in my app, similar to that of the list of alarms in the 'Clock' app from apple. However, each time I get the array of notifications and attempt to display them, they are not being correctly displayed all the time.
Each notification is repeated each day at the same time, so I am using UNUserNotificationCenter.current().getPendingNotificationRequests to get an array of the notifications. With this array of notifications, I iterate over each notification, create a new custom 'Reminder' object and add it to my array of 'Reminders' which I use when I display the notifications in the table view controller.
I do this every time using the viewWillAppear function.
Here is the code:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
generateReminders()
tableView.reloadData()
}
func generateReminders()
{
let center = UNUserNotificationCenter.current()
center.getPendingNotificationRequests { (notifications) in
for item in notifications {
if let trigger = item.trigger as? UNCalendarNotificationTrigger,
let triggerDate = trigger.nextTriggerDate() {
var withSound = true
if(item.content.sound != UNNotificationSound.default)
{
withSound = false
}
self.reminders.append(Reminder(identifier: item.identifier, time: triggerDate, message: item.content.body, withSound: withSound, isAPendingNotification: true))
}
}
self.remindersCount = notifications.count
}
}
When the cells are about to be displayed in the table view controller, I use the 'Reminder' array to customise each cell to display the information of the notification. This is all done in the 'cellForRowAt' function, code below.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Reminder", for: indexPath)
var text = ""
var detailText = ""
if(indexPath.row < remindersCount) {
let reminder = reminders[indexPath.row]
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm"
text = formatter.string(from: reminder.Time)
detailText = reminder.Message
}
cell.textLabel?.text = text
cell.detailTextLabel?.text = detailText
return cell
}
When a user selects another tab to view, I reset the 'reminders' object to be empty so that when they return to this tab, an updated array of notifications is displayed, code below.
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
reminders = [Reminder]()
remindersCount = 0
tableView.setNeedsDisplay()
}
The issue I am facing is this is extremely inconsistent, sometimes all the notifications are displayed, sometimes only some are displayed and other times none of them are displayed. However, each time I print out the count of the number of notifications in the UNUserNotificationCenter.current().getPendingNotificationRequests method it is always the correct number. Furthermore, whenever I click on a cell that should contain information about a notification, the information is there, it is just not being displayed.
Here is a short video of these issues.
https://imgur.com/1RVerZD
I am unsure how to fix this, I have attempted to run the code on the main queue and on the global queue with the quality of service set to '.userInteractive' as shown below, but, still no dice.
let center = UNUserNotificationCenter.current()
let dq = DispatchQueue.global(qos: .userInteractive)
dq.async {
center.getPendingNotificationRequests { (notifications) in
for item in notifications {
if let trigger = item.trigger as? UNCalendarNotificationTrigger,
let triggerDate = trigger.nextTriggerDate() {
var withSound = true
if(item.content.sound != UNNotificationSound.default)
{
withSound = false
}
self.reminders.append(Reminder(identifier: item.identifier, time: triggerDate, message: item.content.body, withSound: withSound, isAPendingNotification: true))
}
}
self.remindersCount = notifications.count
}
}
A small application of this issue occurring can be downloaded from this Github repository.
https://github.com/AlexMarchant98/LitstingNotificationsIssue
There are some issues in your code of tableview as listed below.
You used static cells in your tableview which is not the proper way if you have dynamic rows.
Suggestion : Use Dynamic Prototype Cell for your tableview.
remindersCount not required at all as its already there in your array count.
Suggestion : Use self.reminders.count for array count.
unwindToRemindersTableViewController() method have generateReminders() call which is not required as viewWillAppear() will call when view dismiss.
Suggestion : Check ViewController life cycle you will get proper idea how to reload data.
I have updated some code in your sample project.
Please find updated code here.
Github updated demo
Hope this will helps!
The problem with your code is the timing when tableView.reloadData() gets executed.
UNUserNotificationCenter.current().getPendingNotificationRequests() is a asynchronous call and therefore the reminders array gets filled after tableView.reloadData() was called.
Moving tableView.reloadData() to the end of the callback-block of getPendingNotificationRequests() should fix your issue. (And don't forget to trigger the reloadData() from the main thread)
func generateReminders()
{
let center = UNUserNotificationCenter.current()
let dq = DispatchQueue.global(qos: .userInteractive)
dq.async {
center.getPendingNotificationRequests { (notifications) in
for item in notifications {
if let trigger = item.trigger as? UNCalendarNotificationTrigger,
let triggerDate = trigger.nextTriggerDate() {
var withSound = true
if(item.content.sound != UNNotificationSound.default)
{
withSound = false
}
self.reminders.append(Reminder(identifier: item.identifier, time: triggerDate, message: item.content.body, withSound: withSound, isAPendingNotification: true))
}
}
self.remindersCount = notifications.count
DispatchQueue.main.async {
self.tableView.reloadData() // <---------
}
}
}
}

Page created by MFMailComposeViewController could not be shown because of hierarchy - Swift 4

I attempt to add a function, that is a mail page would pop up after the user touched a row in a table. Namely, it means that the user could activate a "function" (here the name of that function is "orderOfSendAnEmailToReportTheProblem") when the row is tapped. All of my codes were shown below. (This kind of code has been proposed by several genii on Stackoverflow...)
import Foundation
import UIKit
import MessageUI
class ReportProblem : UIViewController, MFMailComposeViewControllerDelegate {
func orderOfSendAnEmailToReportTheProblem() {
let mailComposeViewController = configureMailController()
if MFMailComposeViewController.canSendMail() {
self.present(mailComposeViewController, animated: true, completion: nil)
} else {
showMailError()
}
}
//Activate the series of the commands of sending the email.
func configureMailController() -> MFMailComposeViewController {
let mailComposeVC = MFMailComposeViewController()
mailComposeVC.mailComposeDelegate = self
mailComposeVC.setToRecipients(["my email"])
mailComposeVC.setSubject("Yo")
return mailComposeVC
}
//Set the recipient and the title of this email automatically.
func showMailError() {
let sendMailErrorAlert = UIAlertController(title: "Could not sned the email.", message: "Oops, something was wrong, please check your internet connection once again.", preferredStyle: .alert)
let dismiss = UIAlertAction(title: "Ok", style: .default, handler: nil)
sendMailErrorAlert.addAction(dismiss)
self.present(sendMailErrorAlert, animated: true, completion: nil) //If you conform the protocol of NSObject instead of UIViewController, you could not finish this line successfully.
}
//Set a alert window so that it would remind the user when the device could not send the email successfully.
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
controller.dismiss(animated: true, completion: nil)
}
//Set this final step so that the device would go to the previous window when you finish sending the email.
}
However, a problem occurred. When I test it on my real device, and after I tapped that particular row, nothing happened, no any new page pop up... The Xcode only showed that "Warning: Attempt to present on whose view is not in the window hierarchy!" I have tried several ways, such as "view.bringSubview(toFront: mailComposeVC)" or adding the codes shown below at the end of my codes, but nothing worked.
func topMostController() -> UIViewController {
var topController: UIViewController = UIApplication.shared.keyWindow!.rootViewController!
while (topController.presentedViewController != nil) {
topController = topController.presentedViewController!
}
return topController
}
I noticed that some other people also would face similar problems when they want to create the alert window, and the solution of that is to create an independent UIWindow, but I want to use mailComposeController to present the email page instead. Some others also faced some problems about MFMailComposeViewController, but their problems are not concerning to hierarchy. I was a novice of swift, and I was haunted by this problem for a whole day... I used swift 4 to develop my App, is anyone know how to solve this problem here?...
So now I'm writing another way to present which I'm using for generic views.
Have Some code in another class for presentation of view so that you can reuse them throughout the app with these two methods.
func slideInFromRight(parentView:UIView,childView:UIView) {
childView.transform = CGAffineTransform(translationX: parentView.frame.maxX, y: 0)
parentView.addSubview(childView)
UIView.animate(withDuration: 0.25, animations: {
childView.transform = CGAffineTransform(translationX: 0, y: 0)
})
}
func slideOutToRight(view:UIView) {
UIView.animate(withDuration: 0.25, animations: {
view.transform = CGAffineTransform(translationX: view.frame.maxX, y: 0)
},completion:{(completed:Bool) in
view.removeFromSuperview()
})
}
Now use these methods to present and remove custom view controller as follows
let window = UIApplication.shared.keyWindow
let vc = YourViewController().instantiate()
self.addChildViewController(vc)
let view = vc.view
view.frame = CGRect(x: 0, y: 20, width: window!.frame.width, height: window!.frame.height-20)
//Here Animation is my custom presenter class and shared is it's shared instance.
Animation.shared.slideInFromRight(parentView: window!, childView: view)
//Or you can use current View controller's view
Animation.shared.slideInFromRight(parentView: self.view!, childView: view)
Genius Vivek Singh, your way looks good, but it's a little bit tedious. Moreover, it still did not work in my project... (It seems that you used some codes about UIView, such as parentView, childView, and view. However, I used MFMailComposeViewController which seems is a little bit different from original view...I am not sure whether this theory is correct or not...)
However, I have found the solution. I presume that the problem is that after the user clicked the row in another tableViewController (here is SettingTVController), it would activate the function "orderOfSendAnEmailToReportTheProblem( )" which is in "another " viewController (here is ReportProblem). Because there are two different viewController, some kind of conflict occurred.
Therefore, I move my whole codes I posted in the above question to my original tableViewController, so that the user would not go into another viewController when they activate the function, and there's no hierarchy problem anymore.
import UIKit
import StoreKit
import MessageUI
class SettingTVController: UITableViewController, MFMailComposeViewControllerDelegate {
var settingTitleConnection = showData()
override func viewDidLoad() {
//skip
}
override func didReceiveMemoryWarning() {
//skip
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//skip
}
override func numberOfSections(in tableView: UITableView) -> Int {
//skip
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
//skip
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if tableView.indexPathForSelectedRow?.row == 2 && tableView.indexPathForSelectedRow?.section == 1 {
orderOfSendAnEmailToReportTheProblem()
} else {
//skip
}
tableView.deselectRow(at: indexPath, animated: true)
}
//-----<The codes below is used to construct the function of reporting problem with email>-----
func orderOfSendAnEmailToReportTheProblem() {
let mailComposeViewController = configureMailController()
self.present(mailComposeViewController, animated: true, completion: nil)
if MFMailComposeViewController.canSendMail() {
self.present(mailComposeViewController, animated: false, completion: nil)
} else {
showMailError()
}
}
//Activate the series of the commands of sending the email.
func configureMailController() -> MFMailComposeViewController {
let mailComposeVC = MFMailComposeViewController()
mailComposeVC.mailComposeDelegate = self
mailComposeVC.setToRecipients(["datototest#icloud.com"])
mailComposeVC.setSubject("Reporting of Problems of Rolling")
return mailComposeVC
}
//Set the recipient and the title of this email automatically.
func showMailError() {
let sendMailErrorAlert = UIAlertController(title: "Could not send the email.", message: "Oops, something was wrong, please check your internet connection once again.", preferredStyle: .alert)
let dismiss = UIAlertAction(title: "Ok", style: .default, handler: nil)
sendMailErrorAlert.addAction(dismiss)
self.present(sendMailErrorAlert, animated: true, completion: nil) //If you conform the protocol of NSObject instead of UIViewController, you could not finish this line successfully.
}
//Set a alert window so that it would remind the user when the device could not send the email successfully.
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
controller.dismiss(animated: true, completion: nil)
//UIApplication.shared.keyWindow?.rootViewController?.dismiss(animated: true, completion: nil)
}
//Set this final step so that the device would go to the previous window when you finish sending the email.
//-----<The codes above is used to construct the function of reporting problem with email>-----
}
I posted my codes above so that it may help others who face similar problem someday. Once again, thanks for your help!!
I don't know why you are facing a view hierarchy issue. But I am able to achieve the mail share option in swift 4. I followed exactly same steps.
Check if mail can be sent:
MFMailComposeViewController.canSendMail()
Configure mail body:
private func configureMailController() -> MFMailComposeViewController {
let mailComposeViewController = MFMailComposeViewController()
mailComposeViewController.mailComposeDelegate = self
mailComposeViewController.setMessageBody("MESSAGE BODY", isHTML: true)
return mailComposeViewController
}
Present mail VC:
present(mailComposeViewController, animated: true)
confirm optional protocol and dismiss the view explicitly:
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
controller.dismiss(animated: true)
}

Why canPerformAction get called again when action of menuItem is called?

Below is my code, I found when click menu "pasteAndGo", two log strings are printed: 1. paste and go show 2.paste and go clicked. My requirement is when the menu is shown, log "paste and go show" is shown. When it is clicked, log "paste and go clicked" is shown.
class MyTextField: UITextField {
private func Init() {
let menuController: UIMenuController = UIMenuController.shared
menuController.isMenuVisible = true
let pasteAndGoMenuItem: UIMenuItem = UIMenuItem(title: "pasteAndGo", action: #selector(pasteAndGo(sender:)))
let myMenuItems: NSArray = [pasteAndGoMenuItem]
menuController.menuItems = myMenuItems as? [UIMenuItem]
}
#objc private func pasteAndGo(sender: UIMenuItem) {
Print("paste and go clicked")
}
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
let pasteboard = UIPasteboard.general
if action == #selector(pasteAndGo) {
if pasteboard.url != nil {
Print("paste and go show")
return true
} else {
return false
}
}
return super.canPerformAction(action, withSender: sender)
}
}
Your code works as implemented:
In the instant you press your pasteAndGo menu item, the UIKit framework calls canPerformAction to ask whether it is allowed to execute the action or not. Here, you print "paste and go show"
Since you return true, your action pasteAndGo(sender:) is executed and prints "paste and go clicked"
To react on the menu item being shown, you'll have to register to the notification center with the UIMenuController.willShowMenuNotification, like this:
// create a property
var token: NSObjectProtocol?
// then add observer
self.token = NotificationCenter.default.addObserver(forName: UIMenuController.willShowMenuNotification, object: nil, queue: .main)
{ _ in
print ("paste and go show")
}
and don't forget to unsubscribe (NotificationCenter.default.removeObserver) once your viewcontroller gets dismissed.
if let t = self.token {
NotificationCenter.default.removeObserver(t)
}
Update
You could also do so (without properties) in Init
// in Init
var token = NotificationCenter.default.addObserver(forName: UIMenuController.willShowMenuNotification, object: nil, queue: .main)
{ _ in
print ("paste and go show")
NotificationCenter.default.removeObserver(token)
}

CNContactViewController Cancel Button Not Working

I'm trying to use the built-in new contact UI and am getting unexpected behavior with the cancel button. The code below works and calls up the new contact screen but the cancel button will only clear the screen entries not cancel out of the new contact screen. In the built in contacts app hitting cancel returns to the contact list screen. I would like the cancel button to close out the window.
#IBAction func newTwo(sender: AnyObject) {
AppDelegate.getAppDelegate().requestForAccess { (accessGranted) -> Void in
if accessGranted {
let npvc = CNContactViewController(forNewContact: nil)
npvc.delegate = self
self.navigationController?.pushViewController(npvc, animated: true)
}
}
}
did you implement CNContactViewControllerDelegate methods?
Here's a link to documentation
for example:
func contactViewController(viewController: CNContactViewController, didCompleteWithContact contact: CNContact?) {
self.dismissViewControllerAnimated(true, completion: nil)
}
It worked for me using the following code:
Swift 3
func contactViewController(_ vc: CNContactViewController, didCompleteWith con: CNContact?) {
vc.dismiss(animated: true)
}
Also I changed the way I was calling the controller:
Instead of:
self.navigationController?.pushViewController(contactViewController, animated: true)
the solution was:
self.present(UINavigationController(rootViewController: contactViewController), animated:true)
I found the solution using the example code Programming-iOS-Book-Examples written by Matt Neuburg:
Better way to do the dismissing would be to check if the contact is nil and then dismiss it. The dismiss doesn't work if you've pushed the view controller from a navigation controller. You might have to do the following:
func contactViewController(viewController: CNContactViewController, didCompleteWithContact contact: CNContact?) {
if let contactCreated = contact
{
}
else
{
_ = self.navigationController?.popViewController(animated: true)
}
}

Resources