I have a lot of troubleshooting these issues a few days ago. Here the complete code,
You need to create 2 Files.
import Foundation
import Contacts
class contactsAspcts {
var contactOut: CNContact
init(contactOut: CNContact) {
self.contactOut = contactOut
}
}
Then Create new files to create TableView Controller
import UIKit
private let cellId = "contactCell"
class ViewController: UITableViewController, UISearchResultsUpdating {
// the contacts array
var allContacts = [contactsAspcts]()
override func viewDidLoad() {
super.viewDidLoad()
setupNavBar()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellId)
fetchData()
searchBarUI()
}
func setupNavBar() {
navigationItem.title = "Your Contacts"
navigationController?.navigationBar.prefersLargeTitles = true
}
//MARK: SEARCH VIEW CONTROLLER - START
var searchViewController: UISearchController = UISearchController(searchResultsController: nil)
var searchResults: [contactsAspcts] = []
func searchBarUI() {
searchViewController.searchResultsUpdater = self
searchViewController.hidesNavigationBarDuringPresentation = true
searchViewController.obscuresBackgroundDuringPresentation = true
searchViewController.searchBar.placeholder = "Search contacts by family name"
searchViewController.searchBar.barTintColor = UIColor.yellow
searchViewController.obscuresBackgroundDuringPresentation = false
definesPresentationContext = true
navigationItem.hidesSearchBarWhenScrolling = false
navigationItem.searchController = searchViewController
}
func updateSearchResults(for searchController: UISearchController) {
let textToBeLowercased = searchViewController.searchBar.text?.lowercased()
filtercontent(for: textToBeLowercased!)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
func filtercontent(for searchText: String) {
searchResults = self.allContacts.filter({ (contact) -> Bool in
return contact.contactOut.givenName.lowercased().range(of: searchText) != nil
})
}
//MARK: SEARCH VIEW CONTROLLER - END
}
extension ViewController {
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 60
}
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headLabel = UILabel()
headLabel.backgroundColor = .black
return headLabel
}
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 10
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if searchViewController.isActive {
return searchResults.count
} else {
return allContacts.count
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if searchViewController.isActive {
let cell = ContactShowCell(style: .subtitle, reuseIdentifier: cellId)
cell.textLabel?.text = "\(searchResults[indexPath.row].contactOut.givenName)" + " \( searchResults[indexPath.row].contactOut.familyName)"
/// This is crashed when fetching it
// company
cell.detailTextLabel?.text = "\(searchResults[indexPath.row].contactOut.jobTitle)" // crashed
// Profile
cell.imageView?.image = UIImage(data: searchResults[indexPath.row].contactOut.imageData!) // crashed
return cell
} else {
let cell = ContactShowCell(style: .subtitle, reuseIdentifier: cellId)
cell.textLabel?.text = "\(allContacts[indexPath.row].contactOut.familyName)" + " " + "\(allContacts[indexPath.row].contactOut.givenName)"
cell.detailTextLabel?.text = (allContacts[indexPath.row].contactOut.phoneNumbers.first?.value.stringValue)
return cell
}
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let nextVC = DetailViewController()
if searchViewController.isActive {
nextVC.familyNamePassedOver = searchResults[indexPath.row].contactOut.familyName
nextVC.givenNamePassedOver = searchResults[indexPath.row].contactOut.givenName
if let phoneToPass = searchResults[indexPath.row].contactOut.phoneNumbers.first?.value.stringValue {
nextVC.phonenumberPassedOver = phoneToPass
}
} else {
nextVC.familyNamePassedOver = allContacts[indexPath.row].contactOut.familyName
nextVC.givenNamePassedOver = allContacts[indexPath.row].contactOut.givenName
if let phoneToPass = allContacts[indexPath.row].contactOut.phoneNumbers.first?.value.stringValue {
nextVC.phonenumberPassedOver = phoneToPass
}
}
navigationController?.pushViewController(nextVC, animated: true)
}
}
import Foundation
import Contacts
import UIKit
extension ViewController {
func fetchData() {
let contactStore = CNContactStore()
contactStore.requestAccess(for: .contacts) { (accessGrant, error) in
if let error = error {
print("there is an error - \(error)")
let alertController = UIAlertController(title: "We need access to your contacts to display them", message: "Go to your settings and grant us permissions", preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil))
self.present(alertController, animated: true, completion: nil)
return
} else {
if accessGrant {
print("access is granted")
let fetchKeys = [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactPhoneNumbersKey]
let request = CNContactFetchRequest(keysToFetch: fetchKeys as [CNKeyDescriptor])
do {
try contactStore.enumerateContacts(with: request) { (retrievedContact, stopPointer) in
let contactObject = contactsAspcts(contactOut: retrievedContact)
self.allContacts.append(contactObject)
}
} catch let error {
print("falied to enumerate" , error)
}
} else {
print("access is denied")
}
}
}
}
}
Note: I split the code of the search by using the if statement
- search has shown a phone number
- Not search show job title
It seems it successfully fetched a phone number (when it disable jobTitle and imageData from CNContacts) However, I enabled them and cause the crash app show up in console:
Terminating app due to uncaught exception 'CNPropertyNotFetchedException', reason: 'A property was not requested when contact was fetched.'
I knew it one issues is this code!
// Company
cell.detailTextLabel?.text = "\(searchResults[indexPath.row].contactOut.jobTitle)" // crashed
// Profile
cell.imageView?.image = UIImage(data: searchResults[indexPath.row].contactOut.imageData!) // crashed
I have no idea. I have a lot of research on that issue with that crash, I have used CNContact which access the photo and Company (or else more) from Contacts.
Thanks!