代码之家  ›  专栏  ›  技术社区  ›  Joachim Kurz

依赖密钥路径的KVO对于Swift类不能正常工作

  •  3
  • Joachim Kurz  · 技术社区  · 7 年前

    我想写封信 URLSessionTask 在Swift中。相符合的 to the documentation

    所有任务属性都支持键值观察。

    因此,我希望保持这种行为,并使包装器上的所有属性也符合KVO(通常委托给包装的任务),并且完全可访问Objective-C。我将描述我对一个属性所做的操作,但我基本上希望对所有属性都做同样的操作。

    我们拿着房子 state 属于 URLSessionTask . 我创建的包装如下:

    @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 以便通过消息传递/运行时甚至从Swift调用属性,以确保可以通过以下方式生成正确的KVO通知 NSObject . 这应该足够了 Apple's KVO chapter in the "Using Swift with Cocoa and Objective-C" book .

    然后我实现了静态类方法 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 :

    ***由于未捕获的异常“nSinternalinconsistenceexception”,正在终止应用程序,原因:“无法删除观察者<_XCKVOExpectationImplementation 0x60000009d6a0>对于键路径“underlyngtask.state”,来自(<MyURLSessionTask 0x6000002a1440>,很可能是因为键“underlyngtask”的值已更改,但没有发送适当的KVO通知。检查MyURLSessionTask类的KVO兼容性。”

    但是通过 underlyingTask -财产 @objc公司 动态 ,Objective-C运行时应确保发送此通知,即使任务已从Swift更改,对吗?

    通过手动发送UnderlyngTask的KVO通知,我可以使测试正常工作,如下所示:

    @objc public internal(set) var underlyingTask: URLSessionTask? {
        willSet {
            willChangeValue(for: \.underlyingTask)
        }
        didSet {
            didChangeValue(for: \.underlyingTask)
        }
    }
    

    但我宁愿避免对每一个属性都实现这一点,而宁愿使用现有的 keyPathsForValuesAffecting<Key> 方法。我是不是错过了让这一切成功的东西?还是应该工作,这是一个bug?

    1 回复  |  直到 7 年前
        1
  •  1
  •   Willeke    7 年前

    所有物 underlyingTask 不是吗 dynamic .