Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

I have a Result type that I use in asynchronous processes:

internal enum Result<T> {

    case success(T)

    case failure(Error)

}

I also have a APIDataResultContext that I use to pass data between Operation subclasses:

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()
        }
    }

}

In my unit tests, I need to determine when result has been set in an APIDataResultContext instance. I can't use KVO because my Result<T> type cannot be marked as dynamic since it can't be represented in Objective-C.

I don't know of another way that will allow me to monitor when result is changed other than using a closure property or a Notification, which I would prefer not to do. I will resort to one of the two if necessary, though.

What other way(s) can I monitor for a change of result?

like image 304
Nick Kohrn Avatar asked Nov 28 '25 12:11

Nick Kohrn


2 Answers

I ended up adding a closure property to 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?()
        }
    }

}

I use the closure in my tests to determine when result has been changed:

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)
}
like image 67
Nick Kohrn Avatar answered Nov 30 '25 05:11

Nick Kohrn


You've already solved this issue (and what you did is probably what I'd do), but there's probably still value in providing a literal answer for the title question: How can you use KVO on a non-Objective-C type?

As it turns out, it's not too difficult to do, although it is somewhat ugly. Basically, you need to create an Objective-C property that is typed Any with the same Objective-C name as the Swift name of the real property. Then, you put willSet and didSet handlers on the real property that call the appropriate KVO methods for the Objective-C property. So, something like:

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

(For the sake of simplicity, I'm assuming that result is your stored property, and removing the lock and the private property from the equation)

The caveat is that you will have to use _resultKVO instead of result when constructing key paths to observe, which means that if this needs to be observable from outside the object, you can't make _resultKVO private, and you'll have to clutter up your class's interface with it. But so it goes.

Again, I probably wouldn't do this for your particular use case (and if you did, you could obviously fire the notifications in result's set rather than having to bother with willSet and didSet), but in some cases this can be useful, and it's good to have an answer describing how to do it as a reference.

like image 43
Charles Srstka Avatar answered Nov 30 '25 06:11

Charles Srstka



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!