CXCallDirectoryProvider拦截号码自动失效,如何实现永久拦截至手动移除?
问题分析与修复方案
你的问题核心是CallKit拦截列表无法持久化,首次生效后号码自动消失,根源在于代码中几个关键逻辑错误:
关键问题点
号码排序不符合CallKit要求
CallKit明确要求阻塞号码必须按数值升序提供,但你的代码中使用了降序排序:sorted(by: { $0.phone > $1.phone})错误的排序会导致CallKit无法正确解析拦截列表,部分号码会被忽略或丢失。
增量请求逻辑混乱
addOrRemoveIncrementalIdentificationPhoneNumbers方法是用于处理号码标识的,你却在里面重复实现了阻塞号码的添加/移除逻辑,导致增量请求时重复操作,覆盖正确的拦截状态。- 增量请求应该只处理从上一次加载后发生变更的号码,而不是全量添加/移除所有号码。
静态变量
lockType不可靠
扩展进程可能被系统回收重启,静态变量会被重置,导致增量请求时无法正确判断操作类型。应该依赖Realm中isBlocked字段的持久化状态来跟踪需要变更的号码。Realm操作不规范
- 循环中逐个写Realm,性能差且容易引发错误;
- 使用
try?吞掉错误,无法排查Realm操作失败的问题; - 每次加载列表时强制修改
isBlocked,会导致状态和实际拦截列表不一致。
修复后的代码
1. 先给Number模型新增追踪字段
用于记录号码状态的变更时间,支持增量更新判断:
class Number: Object { @Persisted var phone: String = "" @Persisted var isBlocked: Bool = false @Persisted var lastUpdated: Date = Date() // 新增字段,记录状态变更时间 }
2. 修复CallDirectoryHandler核心逻辑
enum LockAndUnlockType { case lock case unlock } class CallDirectoryHandler: CXCallDirectoryProvider { // 保存上次加载的时间戳,用于增量请求判断 private let lastLoadTimestampKey = "CallDirectoryLastLoadTimestamp" override func beginRequest(with context: CXCallDirectoryExtensionContext) { context.delegate = self let lastLoadTimestamp = UserDefaults.standard.object(forKey: lastLoadTimestampKey) as? Date ?? .distantPast if context.isIncremental { handleIncrementalUpdates(context: context, since: lastLoadTimestamp) } else { handleFullUpdates(context: context) } // 更新本次加载时间戳 UserDefaults.standard.set(Date(), forKey: lastLoadTimestampKey) context.completeRequest() } // 全量加载所有需要阻塞的号码 private func handleFullUpdates(context: CXCallDirectoryExtensionContext) { // 按升序排序,符合CallKit要求 let blockedNumbers = RealmDataBase.shered.fetchAllObjectsAt(Number.self) .filter { $0.isBlocked } .uniqued() .sorted(by: { $0.phone < $1.phone }) for number in blockedNumbers { if let phoneNumber = CXCallDirectoryPhoneNumber(number.phone) { context.addBlockingEntry(withNextSequentialPhoneNumber: phoneNumber) } } // 保留原有的号码标识逻辑 addAllIdentificationPhoneNumbers(to: context) } // 增量加载上次加载后状态变更的号码 private func handleIncrementalUpdates(context: CXCallDirectoryExtensionContext, since lastLoadDate: Date) { let realm = try! Realm() // 获取上次加载后状态有变化的号码 let updatedNumbers = realm.objects(Number.self) .filter("lastUpdated > %@", lastLoadDate) .uniqued() for number in updatedNumbers { guard let phoneNumber = CXCallDirectoryPhoneNumber(number.phone) else { continue } if number.isBlocked { context.addBlockingEntry(withNextSequentialPhoneNumber: phoneNumber) } else { context.removeBlockingEntry(withPhoneNumber: phoneNumber) } } } // 原号码标识逻辑保留 private func addAllIdentificationPhoneNumbers(to context: CXCallDirectoryExtensionContext) { let allPhoneNumbers: [CXCallDirectoryPhoneNumber] = [1_877_555_5555, 1_888_555_5555] let labels = ["Telemarketer", "Local business"] for (phoneNumber, label) in zip(allPhoneNumbers, labels) { context.addIdentificationEntry(withNextSequentialPhoneNumber: phoneNumber, label: label) } } } extension CallDirectoryHandler: CXCallDirectoryExtensionContextDelegate { func requestFailed(for extensionContext: CXCallDirectoryExtensionContext, withError error: Error) { // 打印错误信息,便于排查问题 print("Call Directory 请求失败: \(error.localizedDescription)") } } // 给Number模型扩展状态更新方法,确保修改状态时同步更新时间戳 extension Number { func updateBlockStatus(_ isBlocked: Bool) { let realm = try! Realm() try! realm.write { self.isBlocked = isBlocked self.lastUpdated = Date() } } }
使用说明
- 添加拦截号码:调用
number.updateBlockStatus(true),然后触发CXCallDirectoryManager.sharedInstance.reloadExtension,拦截列表会持久化该号码。 - 手动移除拦截:调用
number.updateBlockStatus(false),再触发reloadExtension,增量请求会自动从拦截列表中移除该号码。 - 测试验证:可以通过重启设备或在设置中开关扩展,验证增量加载逻辑是否正常工作。
内容的提问来源于stack exchange,提问作者Tigran Vilasyan
相关产品推荐
相关产品推荐

