通讯录管理器

/// 通讯录管理器,负责权限检查、联系人读取、变化监听等功能
final class ContactManager {
    
    // MARK: - 单例与初始化
    static let shared = ContactManager()
    private let contactStore = CNContactStore()
    weak var delegate: ContactManagerDelegate?
    
    // 用户默认存储,用于保存同步状态
    private let userDefaults = UserDefaults.standard
    private let lastSyncDateKey = "LastContactSyncDate"
    private let syncAnchorKey = "LastContactSyncAnchor"
    
    private init() {
        setupNotifications()
    }
    
    deinit {
        NotificationCenter.default.removeObserver(self)
    }
    
    
    let contactKeys: [CNKeyDescriptor] = [
        CNContactIdentifierKey as CNKeyDescriptor,
        CNContactGivenNameKey as CNKeyDescriptor,
        CNContactFamilyNameKey as CNKeyDescriptor,
        CNContactPhoneNumbersKey as CNKeyDescriptor
    ]
}

// MARK: - 权限管理

extension ContactManager {
    
    /// 检查通讯录访问权限
    /// - Parameter completion: 返回权限状态(是否有权限, 是否为limited权限)
    func checkAuthorization(completion: @escaping (Bool, Bool) -> Void) {
        let status = CNContactStore.authorizationStatus(for: .contacts)
        
        switch status {
        case .authorized:
            completion(true, false)
        case .limited:
            completion(true, true)
        case .notDetermined:
            requestAuthorization(completion: completion)
        case .denied, .restricted:
            completion(false, false)
        @unknown default:
            completion(false, false)
        }
    }
    
    /// 弹出提示用户手动去设置页开启完整权限
    func showManualSettingAlert(on viewController: UIViewController) {
        let alert = UIAlertController(
            title: "通讯录访问权限",
            message: "请前往设置 > 隐私 > 通讯录,允许本App访问所有联系人",
            preferredStyle: .alert
        )
        
        alert.addAction(UIAlertAction(title: "取消", style: .cancel))
        alert.addAction(UIAlertAction(title: "去设置", style: .default) { _ in
            SystemFunc.goSetting()
        })
        
        viewController.present(alert, animated: true)
    }
    
    private func requestAuthorization(completion: @escaping (Bool, Bool) -> Void) {
        contactStore.requestAccess(for: .contacts) { granted, _ in
            DispatchQueue.main.async {
                let newStatus = CNContactStore.authorizationStatus(for: .contacts)
                
                if #available(iOS 18.0, *), newStatus == .limited {
                    completion(true, true)
                } else {
                    completion(granted, false)
                }
            }
        }
    }
}

// MARK: - 联系人操作
extension ContactManager {
    
    /// 获取联系人(分页加载)
    /// - Parameters:
    ///   - pageNo: 批次号(0表示获取所有)
    ///   - pageSize: 单次获取的联系人信息条数
    ///   - completion: 返回读取到的联系人数组
    func fetchContacts(pageNo: Int, pageSize: Int, completion: @escaping ([CNContact]) -> Void) {
        // 使用全局队列异步加载联系人数据,确保不阻塞主线程
        DispatchQueue.global(qos: .userInitiated).async { [weak self] in
            guard let self = self else { return }
            
            var contacts = [CNContact]()  // 存储联系人数组
            let request = CNContactFetchRequest(keysToFetch: contactKeys)  // 创建联系人请求,指定需要获取的字段
            request.sortOrder = .givenName  // 按照名字排序
            
            let startIndex = (pageNo - 1) * pageSize
            let endIndex = pageNo * pageSize
            var count = 0  // 计数器,用于记录已经遍历的联系人数量
            
            do {
                // 枚举联系人,获取指定字段的数据
                try self.contactStore.enumerateContacts(with: request) { contact, stop in
                    // 如果 batch 不为 0,并且当前联系人在当前批次范围内,加入结果数组
                    if pageNo > 0 && count >= endIndex {
                        stop.pointee = true  // 停止枚举
                    }
                    
                    if pageNo == 0 || (count >= startIndex && count < endIndex) {
                        contacts.append(contact)
                    }
                    count += 1  // 每次遍历联系人时计数器加1
                }
            } catch {
                print("⚠️ 读取联系人失败: \(error.localizedDescription)")  // 捕获并打印错误
            }
            
            // 在主线程回调,更新UI
            DispatchQueue.main.async {
                completion(contacts)
            }
        }
    }
    
    /// 获取只包含电话号码的联系人
    func fetchContactsWithPhoneNumbers(pageNo: Int, pageSize: Int, completion: @escaping ([CNContact]) -> Void) {
        fetchContacts(pageNo: pageNo, pageSize: pageSize) { contacts in
            let filteredContacts = contacts.filter { !$0.phoneNumbers.isEmpty }
            completion(filteredContacts)
        }
    }
    
    /// 按名字或手机号搜索联系人
    func searchContacts(with query: String, completion: @escaping ([CNContact]) -> Void) {
        let lowercasedQuery = query.lowercased()
        
        fetchContacts(pageNo: 0, pageSize: .max) { contacts in
            let filteredContacts = contacts.filter { contact in
                let fullName = "\(contact.givenName) \(contact.familyName)".lowercased()
                let hasMatchingName = fullName.contains(lowercasedQuery)
                
                let hasMatchingPhone = contact.phoneNumbers.contains {
                    $0.value.stringValue.lowercased().contains(lowercasedQuery)
                }
                return hasMatchingName || hasMatchingPhone
            }
            completion(filteredContacts)
        }
    }
}

// MARK: - 通知处理
private extension ContactManager {
    
    private func setupNotifications() {
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(contactStoreDidChange(_:)),
            name: .CNContactStoreDidChange,
            object: nil
        )
    }
    
    @objc func contactStoreDidChange(_ noti: Notification) {
        delegate?.contactManagerDidChangeContacts()
    }
}

/// 通讯录变化代理
protocol ContactManagerDelegate: AnyObject {
    /// 通讯录变化的回调方法
    func contactManagerDidChangeContacts()
}

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容