如何使用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
相关产品推荐
相关产品推荐

