You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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,读取时直接崩溃。

修复方案

  1. 修改fetchData()方法中的fetchKeys数组,把需要读取的所有字段对应的Key补充完整:
    // 原代码
    // let fetchKeys = [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactPhoneNumbersKey]
    
    // 修改后
    let fetchKeys = [
        CNContactGivenNameKey,
        CNContactFamilyNameKey,
        CNContactPhoneNumbersKey,
        CNContactJobTitleKey, // 职位字段拉取Key
        CNContactImageDataKey // 头像字段拉取Key
    ] as [CNKeyDescriptor]
    
  2. 修复头像读取的强制解包问题:部分联系人未设置头像,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)
    }
    
  3. 注意:所有后续需要读取的CNContact字段(比如公司名、邮箱、生日等),都需要提前把对应的Key加入fetchKeys数组,否则都会触发同类崩溃。

内容的提问来源于stack exchange,提问作者Antonio A. Chavez

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 18:48:18