如何在非 Objective-C 类型上匹配 Swift 4 KVO?

How Can I Match Swift 4 KVO on Non-Objective-C Type?

我有一个在异步进程中使用的 Result 类型:

internal enum Result<T> {

    case success(T)

    case failure(Error)

}

我还有一个 APIDataResultContext 用于在 Operation 子类之间传递数据:

internal final class APIDataResultContext: NSObject {

    // MARK: Properties

    private let lock = NSLock()

    private var _result: Result<Data>!

    internal var result: Result<Data>! {
        get {
            lock.lock()
            let temp = _result
            lock.unlock()
            return temp
        } 
        set {
            lock.lock()
            _result = newValue
            lock.unlock()
        }
    }

}

在我的单元测试中,我需要确定何时在 APIDataResultContext 实例中设置了 result。我不能使用 KVO,因为我的 Result<T> 类型不能标记为 dynamic 因为它不能在 Objective-C 中表示。

除了使用闭包 属性 或 Notification 之外,我不知道还有什么其他方法可以让我监控何时更改 result,我不希望这样做去做。不过,如果有必要,我会使用两者之一。

我可以通过哪些其他方式监控 result 的变化?

我最后添加了一个闭包 属性 到 APIDataResultContext:

internal final class APIDataResultContext {

    // MARK: Properties

    internal var resultChanged: (()->())?

    private let lock = NSLock()

    private var _result: Result<Data>!

    internal var result: Result<Data>! {
        get {
            lock.lock()
            let temp = _result
            lock.unlock()
            return temp
        }
        set {
            lock.lock()
            _result = newValue
            lock.unlock()
            resultChanged?()
        }
    }

}

我在测试中使用闭包来确定 result 何时被更改:

internal func testNeoWsFeedOperationWithDatesPassesDataToResultContext() {
    let operationExpectation = expectation(description: #function)
    let testData = DataUtility().data(from: "Hello, world!")
    let mockSession = MockURLSession()
    let testContext = APIDataResultContext()
    testContext.resultChanged = {
        operationExpectation.fulfill()
        guard let result = testContext.result else {
            XCTFail("Expected result")
            return
        }
        switch result {
        case .failure(_):
            XCTFail("Expected data")
        case .success(let data):
            XCTAssertEqual(data, testData, "Expected '\(testData)'")
        }
    }
    NeoWsFeedOperation(context: testContext, sessionType: mockSession, apiKey: testAPIKey, startDate: testDate, endDate: testDate).start()
    mockSession.completionHandler?(testData, nil, nil)
    wait(for: [operationExpectation], timeout: 2)
}

你已经解决了这个问题(你所做的可能就是我要做的),但为标题问题提供字面答案可能仍然有价值:如何 你在 non-Objective-C 类型上使用 KVO?

事实证明,做起来并不难,虽然有点难看。基本上,您需要创建一个类型为 Any 的 Objective-C 属性,其 Objective-C 名称与真实 属性 的 Swift 名称相同.然后,将 willSetdidSet 处理程序放在真正的 属性 上,为 Objective-C 属性 调用适当的 KVO 方法。所以,像这样:

@objc(result) private var _resultKVO: Any { return self.result }
internal var result: Result<Data>! {
    willSet { self.willChangeValue(for: \._resultKVO) }
    didSet { self.didChangeValue(for: \._resultKVO) }
}

(为了简单起见,我假设 result 是您存储的 属性,并从等式中删除锁和私有 属性)

需要注意的是,在构建要观察的关键路径时,您必须使用 _resultKVO 而不是 result,这意味着如果需要从 object 外部观察,你不能制作 _resultKVO private,你将不得不用它弄乱 class 的界面。但事情就是这样。

同样,我可能不会针对您的特定用例执行此操作(如果您这样做,您显然可以在 resultset 中触发通知,而不必费心willSetdidSet),但在某些情况下这可能很有用,最好有一个描述如何做的答案作为参考。