Tracking properties in `@Observable` models internally

Hi @prathameshk, I highly recommend you do not use that publisher tool. The use of Task { … } in withObservationTracking in order to get the changed value has potential race conditions. This test shows the problem:

@Observable
class Model {
  var count = 0
}

final class ObservablePublisherTests: XCTestCase {
  func testBasics() async throws {
    let model = Model()
    let countPublisher = model.publisher(keyPath: \Model.count)

    var values: [Int] = []
    let cancellable = countPublisher.sink { int in
      values.append(int)
    }

    let max = 10
    for _ in 1...max {
      model.count += 1
      try await Task.sleep(for: .seconds(0.01))
    }

    XCTAssertEqual(values.count, max)       // ✅
    XCTAssertEqual(values, Array(1...max))  // ❌

    _ = cancellable
  }
}

This test does pass sometimes, but it will fail if you run it repeatedly, and you will get a failure like this:

:stop_sign: XCTAssertEqual failed: ("[1, 2, 3, 4, 5, 6, 7, 7, 9, 10]") is not equal to ("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]")

This shows that the 8 value was not emitted and instead a stale 7 value took its place. This is due to the race condition of using the unstructured Task.

4 Likes