iOS读取CNContact职位头像触发CNPropertyNotFetchedException崩溃
iOS通讯录读取多字段崩溃排查
前几日花费大量时间排查相关问题,以下是完整实现代码,共需创建2个文件。
第一个文件:联系人模型类
import Foundation import Contacts class contactsAspcts { var contactOut: CNContact init(contactOut: CNContact) { self.contactOut = contactOut } }
第二个文件:TableView列表控制器
import UIKit private let cellId = "contactCell" class ViewController: UITableViewController, UISearchResultsUpdating { // 联系人数组 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: 搜索控制器模块 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: 搜索控制器模块结束 } 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)" /// 此处读取触发崩溃 // 职位 cell.detailTextLabel?.text = "\(searchResults[indexPath.row].contactOut.jobTitle)" // 崩溃点 // 头像 cell.imageView?.image = UIImage(data: searchResults[indexPath.row].contactOut.imageData!) // 崩溃点 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") } } } } }
逻辑说明
通过if语句拆分不同搜索状态下的代码逻辑:
- 搜索状态下展示联系人职位头衔
- 非搜索状态下展示联系人电话号码
问题现象
测试发现:当注释掉CNContact的jobTitle和imageData相关读取代码时,应用可正常获取并展示电话号码;但一旦启用这两个字段的读取,应用就会崩溃,控制台输出如下报错:
Terminating app due to uncaught exception 'CNPropertyNotFetchedException', reason: 'A property was not requested when contact was fetched.'
崩溃根因
使用Contacts框架拉取联系人时,必须在发起拉取请求前,把所有后续要读取的联系人字段对应的Key全部加入拉取参数中,否则读取未提前声明的字段就会触发CNPropertyNotFetchedException异常。当前代码的拉取参数仅声明了姓名、电话3个字段,没有加入职位、头像对应的拉取Key,读取时直接崩溃。
修复方案
- 修改
fetchData()方法中的fetchKeys数组,把需要读取的所有字段对应的Key补充完整:// 原代码 // let fetchKeys = [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactPhoneNumbersKey] // 修改后 let fetchKeys = [ CNContactGivenNameKey, CNContactFamilyNameKey, CNContactPhoneNumbersKey, CNContactJobTitleKey, // 职位字段拉取Key CNContactImageDataKey // 头像字段拉取Key ] as [CNKeyDescriptor] - 修复头像读取的强制解包问题:部分联系人未设置头像,
imageData为nil,直接强制解包会触发新的崩溃,将崩溃处的头像读取代码改为可选绑定写法:// 原代码 // cell.imageView?.image = UIImage(data: searchResults[indexPath.row].contactOut.imageData!) // 修改后 if let avatarData = searchResults[indexPath.row].contactOut.imageData { cell.imageView?.image = UIImage(data: avatarData) } - 注意:所有后续需要读取的CNContact字段(比如公司名、邮箱、生日等),都需要提前把对应的Key加入
fetchKeys数组,否则都会触发同类崩溃。
内容的提问来源于stack exchange,提问作者Antonio A. Chavez
相关产品推荐
相关产品推荐

