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

CXCallDirectoryProvider拦截号码自动失效,如何实现永久拦截至手动移除?

问题分析与修复方案

你的问题核心是CallKit拦截列表无法持久化,首次生效后号码自动消失,根源在于代码中几个关键逻辑错误:


关键问题点

  1. 号码排序不符合CallKit要求
    CallKit明确要求阻塞号码必须按数值升序提供,但你的代码中使用了降序排序:

    sorted(by: { $0.phone > $1.phone})
    

    错误的排序会导致CallKit无法正确解析拦截列表,部分号码会被忽略或丢失。

  2. 增量请求逻辑混乱

    • addOrRemoveIncrementalIdentificationPhoneNumbers方法是用于处理号码标识的,你却在里面重复实现了阻塞号码的添加/移除逻辑,导致增量请求时重复操作,覆盖正确的拦截状态。
    • 增量请求应该只处理从上一次加载后发生变更的号码,而不是全量添加/移除所有号码。
  3. 静态变量lockType不可靠
    扩展进程可能被系统回收重启,静态变量会被重置,导致增量请求时无法正确判断操作类型。应该依赖Realm中isBlocked字段的持久化状态来跟踪需要变更的号码。

  4. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 03:57:07