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.
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)
}
I'm creating an application that allows the user to send a simple email via MFMailComposer. I've already configured that part but i'm trying to figure out, once the email is sent, how do i update the table view showing something like "Email sent" with possibly a time stamp?
so far i have a string array that should store the data for emails sent:
var emailSent = [String]()
Any suggestion or help would truly be appreciated!
You could create a property in your tableview controller called selectedCellIndexPathRow which you would set in your didSelectRowAtIndexPath method:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
selectedCellIndexPathRow = indexPath.row
//Open your MailComposeViewController
}
Then, you could do the following once the email is sent:
func mailComposeController(controller: MFMailComposeViewController, didFinishWithResult result: MFMailComposeResult, error: NSError?) {
if result.rawValue == MFMailComposeResult.Sent.rawValue {
emailSent[selectedCellIndexPathRow] = true
tableView.reloadData()
}
// Dismiss the mail compose view controller.
controller.dismissViewControllerAnimated(true, completion: nil)
}
You could create a custom subclass of MFMailComposerViewController, override setMessageBody and store the sent body for late usage:
class MyMailComposer: MFMailComposeViewController {
var lastSentBody:String?
override func setMessageBody(_ body: String, isHTML: Bool) {
lastSentBody = body
super.setMessageBody(body, isHTML: isHTML)
}
}
Currently I'm developing my first iOS App and I'm a little slow and rude about the code (it's so weird and different from java) and, if this was the only problem, with the new update, Xcode is making my code insane. I think I solved most of the issues but...
Before, on one of the screens, the app opened a the address book and let the user click on one; when the clicked was done, the contact list close and data from that contact was retrieved to the controller. Now, if the user click on a contact, more info is displayed but any information come out of the console log.
I try everything I find on net and I'm not sure why is not working.
Before, I use Addressbook (or something like that) but I already tried with CNContact.
This is the Button code
#IBAction func addNewContactOnClick(_ sender: AnyObject) {
let peoplePicker = CNContactPickerViewController()
peoplePicker.delegate = self
self.present(peoplePicker, animated: true, completion: nil)
}
CNContactPickerDelegate methods
func contactPicker(picker: CNContactPickerViewController, didSelectContacts contacts: [CNContact]){
contacts.forEach { contact in
for number in contact.phoneNumbers {
let phoneNumber = number.value as! CNPhoneNumber
print("number is = \(phoneNumber)")
}
}
}
func contactPickerDidCancel(picker: CNContactPickerViewController) {
print("Cancel Contact Picker")
}
Methods of CNContactPickerDelegate is changed in Swift 3 like below.
func contactPicker(_ picker: CNContactPickerViewController, didSelect contacts: [CNContact]) {
//your code
}
func contactPickerDidCancel(_ picker: CNContactPickerViewController) {
//your code
}
For other methods of CNContactPickerDelegate check Apple Documentation.
I have picked up a contact from the address book
Now I need to display the name to the text view UI in the same view controller.
How to extract the name from the ABRecord?
This is my code.
#IBAction func addContact(sender: AnyObject) {
var peoplePicker = ABPeoplePickerNavigationController()
peoplePicker.peoplePickerDelegate = self
self.presentViewController(peoplePicker, animated: true, completion: nil)
}
func peoplePickerNavigationControllerDidCancel(peoplePicker: ABPeoplePickerNavigationController!) {
dismissViewControllerAnimated(true, completion: nil)
}
func peoplePickerNavigationController(peoplePicker: ABPeoplePickerNavigationController!, didSelectPerson person: ABRecord!) {
var name : String! = ABRecordCopyCompositeName(person)
}
Here the function ABRecordCopyCompositeName return unmanaged I can't force cast to String. It says not convertible.
I'm new to iOS dev. please help.
Use takeUnretainedValue() and takeRetainedValue() to get CFString object and cast it to NSString
func peoplePickerNavigationController(peoplePicker: ABPeoplePickerNavigationController!, didSelectPerson person: ABRecord!) {
let nameCFString : CFString = ABRecordCopyCompositeName(person).takeRetainedValue()
let name : NSString = nameCFString as NSString
}
You can also check KBContactsSelection which allows you to search and select multiple contacts and is easily customizable using elegant Builder Pattern.