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

如何使用Swift KeyPath观察fileCompletedCount的进度变化?

问题

我正尝试把老式的Objective-C观察方式换成现代Swift观察方式,但找不到正确的KeyPath。试了下面的代码直接报错:

// 无法运行
progress.observe(\.userInfo[ProgressUserInfoKey.fileCompletedCountKey], changeHandler: {_,_ in
        
})

错误提示:

Foundation/NSObject.swift:132: Fatal error: Could not extract a String from KeyPath \NSProgress.userInfo.<computed 0x00000001a5a1ecf0 (Optional<Any>)>

补充信息:

  • 没法直接观察fileCompletedCount,因为它是计算属性。
  • 我已经手动把相关属性设为动态属性,不认为这是重复问题。

下面是原来的Objective-C风格观察代码示例:

import Cocoa
private var viewControllerObservationContext = 0

class ViewController: NSViewController {

    private var progress: Progress = {
        let progress = Progress()
        progress.kind = .file
        progress.fileTotalCount = 20
        return progress
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        let filesCompletedKeyPath = "userInfo.NSProgressFileCompletedCountKey"
        progress.addObserver(self, forKeyPath: filesCompletedKeyPath, options: .new, context: &viewControllerObservationContext)
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            self.progress.fileCompletedCount = 10
        }
    }

    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
        guard context == &viewControllerObservationContext else {
            super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
            return
        }
        print(object)
        print(keyPath)
    }

    deinit {
        progress.removeObserver(self, forKeyPath: "userInfo.NSProgressFileCompletedCountKey")
    }
}

解决方案

Swift的KeyPath观察不支持直接观察字典下标成员,尤其是userInfo这种[AnyHashable: Any]类型的字典,没法通过下标KeyPath生成有效的KVC兼容路径。这里给两个可行的解决办法:

办法一:用字符串形式的KeyPath

直接沿用原Objective-C风格的字符串KeyPath,适配现代Swift的observe方法:

// 注意:NSProgressFileCompletedCountKey对应Swift里ProgressUserInfoKey.fileCompletedCountKey的rawValue
let observation = progress.observe(\.userInfo, keyPath: "userInfo.\(ProgressUserInfoKey.fileCompletedCountKey.rawValue)", options: [.new]) { (progress, change) in
    if let completedCount = progress.userInfo[ProgressUserInfoKey.fileCompletedCountKey] as? Int {
        print("已完成文件数:\(completedCount)")
    }
}

// 记得在合适时机(比如deinit)取消观察
deinit {
    observation.invalidate()
}

办法二:封装自定义动态属性

如果不想依赖字符串KeyPath,可以给Progress扩展一个动态计算属性,间接访问userInfo里的值:

extension Progress {
    @objc dynamic var fileCompletedCountDynamic: Int {
        get {
            return self.fileCompletedCount
        }
        set {
            self.fileCompletedCount = newValue
        }
    }
}

然后直接观察这个自定义属性:

let observation = progress.observe(\.fileCompletedCountDynamic, options: [.new]) { (progress, change) in
    print("已完成文件数:\(progress.fileCompletedCountDynamic)")
}

// 取消观察
deinit {
    observation.invalidate()
}

方案说明

  • 办法一不用额外封装,直接复用原KVC路径,适合快速迁移代码。
  • 办法二更符合Swift类型安全特性,避免字符串硬编码,适合长期维护的项目。

内容的提问来源于stack exchange,提问作者Marek H

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 17:25:14