diff --git a/Signals/Signal.swift b/Signals/Signal.swift index 9806c90..e796f98 100644 --- a/Signals/Signal.swift +++ b/Signals/Signal.swift @@ -309,3 +309,38 @@ fileprivate func signalsAssert(_ condition: Bool, _ message: String) { #if DEBUG var assertionHandlerOverride:((_ condition: Bool, _ message: String) -> ())? #endif + +/** + A wrapper for a variable that allows observing its value changes in the `KVO` fashion + ### Usage Example: ### + ```` + class MyClass { + // Custom Access Control for setter + private(set) var property = Property(value: 5) + } + ```` + */ +public struct Property { + + /// The underlying signal. + public private(set) var signal: Signal + + /// The current value of the property. + public var value: T { + get { return signal.lastDataFired! } + set { + assert(Thread.isMainThread, "The property mutation must occur from the main thread because UI code might be observing changes") + signal.fire(newValue) + } + } + + public init(value: T) { + signal = Signal(retainLastData: true) + signal.fire(value) + } + + /// Subscribes an observer to the Property and invokes its callback immediately with the current value + public func observe(with observer: AnyObject, callback: @escaping (T) -> Void) { + signal.subscribePast(with: observer, callback: callback) + } +} diff --git a/SignalsTests/SignalsTests.swift b/SignalsTests/SignalsTests.swift index 291bb00..9d2b729 100644 --- a/SignalsTests/SignalsTests.swift +++ b/SignalsTests/SignalsTests.swift @@ -407,3 +407,76 @@ class SignalsTests: XCTestCase { } } } + +class PropertyTests: XCTestCase { + + func test_observe_shouldFireCallbackWithCurrentValue() { + let sut = Property(value: 10) + XCTAssertEqual(sut.value, 10) + let subscription = NSObject() + let expectation = XCTestExpectation(description: "Subscriber should be notified about the update") + sut.observe(with: subscription) { value in + XCTAssertEqual(value, 10) + expectation.fulfill() + } + wait(for: [expectation], timeout: 0.1) + } + + func test_valueSetter_shouldChangeTheValueAndFireSubscriptionCallback() { + var sut = Property(value: 10) + XCTAssertEqual(sut.value, 10) + let subscription = NSObject() + let expectation = XCTestExpectation(description: "Subscriber should be notified about the update") + var dispatchCount = 0 + sut.observe(with: subscription) { value in + dispatchCount += 1 + if dispatchCount > 1 { + XCTAssertEqual(value, 5) + expectation.fulfill() + } + } + sut.value = 5 + wait(for: [expectation], timeout: 0.1) + } + + func test_automaticCancellation() { + var sut = Property(value: "abc") + var subscription: NSObject? = NSObject() + var dispatchCount = 0 + sut.observe(with: subscription!) { value in + dispatchCount += 1 + if dispatchCount > 1 { + XCTFail("Subscription should be cancelled") + } + } + subscription = nil + let expectation = XCTestExpectation(description: "Delay") + DispatchQueue.main.async { + sut.value = "def" + expectation.fulfill() + } + wait(for: [expectation], timeout: 0.1) + } + + func test_manualCancellation() { + var sut = Property(value: false) + let subscription = NSObject() + var dispatchCount = 0 + sut.observe(with: subscription) { value in + dispatchCount += 1 + if dispatchCount > 1 { + XCTFail("Subscription should be cancelled") + } + } + sut.signal.cancelSubscription(for: subscription) + sut.value = true + sut.observe(with: subscription) { value in + dispatchCount += 1 + if dispatchCount > 2 { + XCTFail("Subscription should be cancelled") + } + } + sut.signal.cancelAllSubscriptions() + sut.value = false + } +}