Swift class 的依赖键路径的 KVO 不能正常工作
KVO for dependent key paths does not work properly for Swift class
我正在尝试在 Swift 中围绕 URLSessionTask
编写包装器。根据to the documentation
All task properties support key-value observing.
所以我想保持这种行为并使我的包装器上的所有属性也符合 KVO(通常委派给包装任务)并且完全可供 Objective-C 访问。我将描述我用一个 属性 做什么,但我基本上想对所有属性做同样的事情。
我们取 URLSessionTask
的 属性 state
。我这样创建包装器:
@objc(MyURLSessionTask)
public class TaskWrapper: NSObject {
@objc public internal(set) var underlyingTask: URLSessionTask?
@objc dynamic public var state: URLSessionTask.State {
return underlyingTask?.state ?? backupState
}
// the state to be used when we don't have an underlyingTask
@objc dynamic private var backupState: URLSessionTask.State = .suspended
@objc public func resume() {
if let task = underlyingTask {
task.resume()
return
}
dispatchOnBackgroundQueue {
let task:URLSessionTask = constructTask()
task.resume()
self.underlyingTask = task
}
}
}
我将 @objc
添加到属性中,以便可以从 Objective-C 调用它们。我将 dynamic
添加到属性中,因此它们将通过 message-passing/the 运行time 甚至从 Swift 调用,以确保 [= 可以生成正确的 KVO-Notifications 21=]。根据 Apple's KVO chapter in the "Using Swift with Cocoa and Objective-C" book,这应该足够了。
然后我实现了静态 class 方法 necessary to tell KVO about dependent key paths:
// MARK: KVO Support
extension TaskWrapper {
@objc static var keyPathsForValuesAffectingState:Set<String> {
let keypaths:Set<String> = [
#keyPath(TaskWrapper.backupState),
#keyPath(TaskWrapper.underlyingTask.state)
]
return keypaths
}
}
然后我写了一个单元测试来检查通知是否被正确调用:
var swiftKVOObserver:NSKeyValueObservation?
func testStateObservation() {
let taskWrapper = TaskWrapper()
let objcKVOExpectation = keyValueObservingExpectation(for: taskWrapper, keyPath: #keyPath(TaskWrapper.state), handler: nil)
let swiftKVOExpectation = expectation(description: "Expect Swift KVO call for `state`-change")
swiftKVOObserver = taskWrapper.observe(\.state) { (_, _) in
swiftKVOExpectation.fulfill()
}
// this should trigger both KVO versions
taskWrapper.underlyingTask = URLSession(configuration: .default).dataTask(with: url)
self.wait(for: [swiftKVOExpectation, objcKVOExpectation], timeout: 0.1)
}
当我 运行 它时,测试崩溃 NSInternalInconsistencyException
:
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Cannot remove an observer <_XCKVOExpectationImplementation 0x60000009d6a0> for the key path "underlyingTask.state" from < MyURLSessionTask 0x6000002a1440>, most likely because the value for the key "underlyingTask" has changed without an appropriate KVO notification being sent. Check the KVO-compliance of the MyURLSessionTask class.'
但是通过underlyingTask
-属性@objc
和dynamic
,Objective-C运行时间应该确保这个通知是已发送,即使任务从 Swift 更改,对吗?
我可以通过像这样手动发送底层任务的 KVO 通知来使测试正常工作:
@objc public internal(set) var underlyingTask: URLSessionTask? {
willSet {
willChangeValue(for: \.underlyingTask)
}
didSet {
didChangeValue(for: \.underlyingTask)
}
}
但我更愿意避免为每个 属性 实现它,并且更愿意使用现有的 keyPathsForValuesAffecting<Key>
方法。我是否遗漏了一些东西来完成这项工作?或者它应该工作,这是一个错误?
属性 underlyingTask
不是 dynamic
.
我正在尝试在 Swift 中围绕 URLSessionTask
编写包装器。根据to the documentation
All task properties support key-value observing.
所以我想保持这种行为并使我的包装器上的所有属性也符合 KVO(通常委派给包装任务)并且完全可供 Objective-C 访问。我将描述我用一个 属性 做什么,但我基本上想对所有属性做同样的事情。
我们取 URLSessionTask
的 属性 state
。我这样创建包装器:
@objc(MyURLSessionTask)
public class TaskWrapper: NSObject {
@objc public internal(set) var underlyingTask: URLSessionTask?
@objc dynamic public var state: URLSessionTask.State {
return underlyingTask?.state ?? backupState
}
// the state to be used when we don't have an underlyingTask
@objc dynamic private var backupState: URLSessionTask.State = .suspended
@objc public func resume() {
if let task = underlyingTask {
task.resume()
return
}
dispatchOnBackgroundQueue {
let task:URLSessionTask = constructTask()
task.resume()
self.underlyingTask = task
}
}
}
我将 @objc
添加到属性中,以便可以从 Objective-C 调用它们。我将 dynamic
添加到属性中,因此它们将通过 message-passing/the 运行time 甚至从 Swift 调用,以确保 [= 可以生成正确的 KVO-Notifications 21=]。根据 Apple's KVO chapter in the "Using Swift with Cocoa and Objective-C" book,这应该足够了。
然后我实现了静态 class 方法 necessary to tell KVO about dependent key paths:
// MARK: KVO Support
extension TaskWrapper {
@objc static var keyPathsForValuesAffectingState:Set<String> {
let keypaths:Set<String> = [
#keyPath(TaskWrapper.backupState),
#keyPath(TaskWrapper.underlyingTask.state)
]
return keypaths
}
}
然后我写了一个单元测试来检查通知是否被正确调用:
var swiftKVOObserver:NSKeyValueObservation?
func testStateObservation() {
let taskWrapper = TaskWrapper()
let objcKVOExpectation = keyValueObservingExpectation(for: taskWrapper, keyPath: #keyPath(TaskWrapper.state), handler: nil)
let swiftKVOExpectation = expectation(description: "Expect Swift KVO call for `state`-change")
swiftKVOObserver = taskWrapper.observe(\.state) { (_, _) in
swiftKVOExpectation.fulfill()
}
// this should trigger both KVO versions
taskWrapper.underlyingTask = URLSession(configuration: .default).dataTask(with: url)
self.wait(for: [swiftKVOExpectation, objcKVOExpectation], timeout: 0.1)
}
当我 运行 它时,测试崩溃 NSInternalInconsistencyException
:
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Cannot remove an observer <_XCKVOExpectationImplementation 0x60000009d6a0> for the key path "underlyingTask.state" from < MyURLSessionTask 0x6000002a1440>, most likely because the value for the key "underlyingTask" has changed without an appropriate KVO notification being sent. Check the KVO-compliance of the MyURLSessionTask class.'
但是通过underlyingTask
-属性@objc
和dynamic
,Objective-C运行时间应该确保这个通知是已发送,即使任务从 Swift 更改,对吗?
我可以通过像这样手动发送底层任务的 KVO 通知来使测试正常工作:
@objc public internal(set) var underlyingTask: URLSessionTask? {
willSet {
willChangeValue(for: \.underlyingTask)
}
didSet {
didChangeValue(for: \.underlyingTask)
}
}
但我更愿意避免为每个 属性 实现它,并且更愿意使用现有的 keyPathsForValuesAffecting<Key>
方法。我是否遗漏了一些东西来完成这项工作?或者它应该工作,这是一个错误?
属性 underlyingTask
不是 dynamic
.