Skip to content

Commit

Permalink
Merge pull request ReactiveCocoa#30 from ReactiveCocoa/any-binding-ta…
Browse files Browse the repository at this point in the history
…rget

Introduce `BindingTarget` (class).
  • Loading branch information
liscio authored Sep 28, 2016
2 parents 21f7720 + fed4d07 commit 33f8a6d
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 4 deletions.
8 changes: 8 additions & 0 deletions ReactiveSwift.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@
7DFBED2F1CDB8DE300EE435B /* TestError.swift in Sources */ = {isa = PBXBuildFile; fileRef = B696FB801A7640C00075236D /* TestError.swift */; };
7DFBED301CDB8DE300EE435B /* TestLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = C79B64731CD38B2B003F2376 /* TestLogger.swift */; };
7DFBED6D1CDB8F7D00EE435B /* SignalProducerNimbleMatchers.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA6B94A1A76044800C846D1 /* SignalProducerNimbleMatchers.swift */; };
9A1D067D1D948A2300ACF44C /* UnidirectionalBindingSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A1D067C1D948A2200ACF44C /* UnidirectionalBindingSpec.swift */; };
9A1D067E1D948A2300ACF44C /* UnidirectionalBindingSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A1D067C1D948A2200ACF44C /* UnidirectionalBindingSpec.swift */; };
9A1D067F1D948A2300ACF44C /* UnidirectionalBindingSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A1D067C1D948A2200ACF44C /* UnidirectionalBindingSpec.swift */; };
9ABCB1851D2A5B5A00BCA243 /* Deprecations+Removals.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ABCB1841D2A5B5A00BCA243 /* Deprecations+Removals.swift */; };
9ABCB1861D2A5B5A00BCA243 /* Deprecations+Removals.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ABCB1841D2A5B5A00BCA243 /* Deprecations+Removals.swift */; };
9ABCB1871D2A5B5A00BCA243 /* Deprecations+Removals.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ABCB1841D2A5B5A00BCA243 /* Deprecations+Removals.swift */; };
Expand Down Expand Up @@ -220,6 +223,7 @@
57A4D2461BA13F9700F7D4B1 /* tvOS-Framework.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "tvOS-Framework.xcconfig"; sourceTree = "<group>"; };
57A4D2471BA13F9700F7D4B1 /* tvOS-StaticLibrary.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "tvOS-StaticLibrary.xcconfig"; sourceTree = "<group>"; };
7DFBED031CDB8C9500EE435B /* ReactiveSwiftTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ReactiveSwiftTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
9A1D067C1D948A2200ACF44C /* UnidirectionalBindingSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnidirectionalBindingSpec.swift; sourceTree = "<group>"; };
9ABCB1841D2A5B5A00BCA243 /* Deprecations+Removals.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Deprecations+Removals.swift"; sourceTree = "<group>"; };
A97451331B3A935E00F48E55 /* watchOS-Application.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "watchOS-Application.xcconfig"; sourceTree = "<group>"; };
A97451341B3A935E00F48E55 /* watchOS-Base.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "watchOS-Base.xcconfig"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -472,6 +476,7 @@
D8170FC01B100EBC004192AD /* FoundationExtensionsSpec.swift */,
4A0E11031D2A95200065D310 /* LifetimeSpec.swift */,
D0A2260D1A72F16D00D33B74 /* PropertySpec.swift */,
9A1D067C1D948A2200ACF44C /* UnidirectionalBindingSpec.swift */,
D0C312F219EF2A7700984962 /* SchedulerSpec.swift */,
02D260291C1D6DAF003ACC61 /* SignalLifetimeSpec.swift */,
D8024DB11B2E1BB0005E6B9A /* SignalProducerLiftingSpec.swift */,
Expand Down Expand Up @@ -888,6 +893,7 @@
7DFBED2E1CDB8DE300EE435B /* FlattenSpec.swift in Sources */,
7DFBED2F1CDB8DE300EE435B /* TestError.swift in Sources */,
7DFBED301CDB8DE300EE435B /* TestLogger.swift in Sources */,
9A1D067F1D948A2300ACF44C /* UnidirectionalBindingSpec.swift in Sources */,
4A0E11061D2A95200065D310 /* LifetimeSpec.swift in Sources */,
7DFBED6D1CDB8F7D00EE435B /* SignalProducerNimbleMatchers.swift in Sources */,
);
Expand Down Expand Up @@ -961,6 +967,7 @@
D0A226081A72E0E900D33B74 /* SignalSpec.swift in Sources */,
02D2602B1C1D6DB8003ACC61 /* SignalLifetimeSpec.swift in Sources */,
D0C3130C19EF2B1F00984962 /* DisposableSpec.swift in Sources */,
9A1D067D1D948A2300ACF44C /* UnidirectionalBindingSpec.swift in Sources */,
D0A2260B1A72E6C500D33B74 /* SignalProducerSpec.swift in Sources */,
D8024DB21B2E1BB0005E6B9A /* SignalProducerLiftingSpec.swift in Sources */,
);
Expand Down Expand Up @@ -1009,6 +1016,7 @@
D0C3131219EF2B2000984962 /* DisposableSpec.swift in Sources */,
CA6F28511C52626B001879D2 /* FlattenSpec.swift in Sources */,
579504341BB8A34300A5E482 /* BagSpec.swift in Sources */,
9A1D067E1D948A2300ACF44C /* UnidirectionalBindingSpec.swift in Sources */,
4A0E11051D2A95200065D310 /* LifetimeSpec.swift in Sources */,
02D2602A1C1D6DAF003ACC61 /* SignalLifetimeSpec.swift in Sources */,
);
Expand Down
2 changes: 1 addition & 1 deletion Sources/Property.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public protocol PropertyProtocol: class {
}

/// Represents an observable property that can be mutated directly.
public protocol MutablePropertyProtocol: PropertyProtocol, BindingTarget {
public protocol MutablePropertyProtocol: PropertyProtocol, BindingTargetProtocol {
/// The current value of the property.
var value: Value { get set }
}
Expand Down
67 changes: 64 additions & 3 deletions Sources/UnidirectionalBinding.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Dispatch
import enum Result.NoError

precedencegroup BindingPrecedence {
Expand All @@ -10,7 +11,7 @@ precedencegroup BindingPrecedence {
infix operator <~ : BindingPrecedence

/// Describes a target to which can be bound.
public protocol BindingTarget: class {
public protocol BindingTargetProtocol: class {
associatedtype Value

/// The lifetime of `self`. The binding operators use this to determine when
Expand Down Expand Up @@ -53,7 +54,7 @@ public protocol BindingTarget: class {
static func <~ <Source: SignalProtocol>(target: Self, signal: Source) -> Disposable? where Source.Value == Value, Source.Error == NoError
}

extension BindingTarget {
extension BindingTargetProtocol {
/// Binds a signal to a target, updating the target's value to the latest
/// value sent by the signal.
///
Expand Down Expand Up @@ -170,7 +171,7 @@ extension BindingTarget {
}
}

extension BindingTarget where Value: OptionalProtocol {
extension BindingTargetProtocol where Value: OptionalProtocol {
/// Binds a signal to a target, updating the target's value to the latest
/// value sent by the signal.
///
Expand Down Expand Up @@ -276,3 +277,63 @@ extension BindingTarget where Value: OptionalProtocol {
return target <~ property.producer
}
}

/// A binding target that can be used with the `<~` operator.
public final class BindingTarget<Value>: BindingTargetProtocol {
public let lifetime: Lifetime
private let setter: (Value) -> Void

/// Creates a binding target.
///
/// - parameters:
/// - lifetime: The expected lifetime of any bindings against the resulting
/// target.
/// - setter: The action to receive values.
public init(lifetime: Lifetime, setter: @escaping (Value) -> Void) {
self.setter = setter
self.lifetime = lifetime
}

/// Creates a binding target which consumes values synchronously on the
/// supplied queue.
///
/// - parameters:
/// - queue: The dispatch queue on which the values are consumed.
/// - lifetime: The expected lifetime of any bindings against the resulting
/// target.
/// - setter: The action to receive values.
public convenience init(on queue: DispatchQueue, lifetime: Lifetime, setter: @escaping (Value) -> Void) {
let queueId = ObjectIdentifier(queue)

/// Ensures the queue has been setup property.
if nil == queue.getSpecific(key: specificKey) {
queue.setSpecific(key: specificKey, value: queueId)
}

let setter: (Value) -> Void = { value in
if queueId == DispatchQueue.getSpecific(key: specificKey) {
setter(value)
} else {
queue.sync {
setter(value)
}
}
}
self.init(lifetime: lifetime, setter: setter)
}

public func consume(_ value: Value) {
setter(value)
}

@discardableResult
public static func <~ <Source: SignalProtocol>(target: BindingTarget<Value>, signal: Source) -> Disposable? where Source.Value == Value, Source.Error == NoError {
return signal
.take(during: target.lifetime)
.observeValues { [setter = target.setter] value in
setter(value)
}
}
}

private let specificKey = DispatchSpecificKey<ObjectIdentifier>()
89 changes: 89 additions & 0 deletions Tests/ReactiveSwiftTests/UnidirectionalBindingSpec.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import Result
import Nimble
import Quick
@testable import ReactiveSwift

class UnidirectionalBindingSpec: QuickSpec {
override func spec() {
describe("BindingTarget") {
var token: Lifetime.Token!
var lifetime: Lifetime!
var target: BindingTarget<Int>!
var value: Int!

beforeEach {
token = Lifetime.Token()
lifetime = Lifetime(token)
target = BindingTarget(lifetime: lifetime, setter: { value = $0 })
value = nil
}

it("should pass through the lifetime") {
expect(target.lifetime).to(beIdenticalTo(lifetime))
}

it("should trigger the supplied setter") {
expect(value).to(beNil())

target.consume(1)
expect(value) == 1
}

it("should accept bindings from properties") {
expect(value).to(beNil())

let property = MutableProperty(1)
target <~ property
expect(value) == 1

property.value = 2
expect(value) == 2
}

it("should not deadlock on the same queue") {
target = BindingTarget(on: .main,
lifetime: lifetime,
setter: { value = $0 })

let property = MutableProperty(1)
target <~ property
expect(value) == 1
}

it("should not deadlock even if the value is originated from the same queue indirectly") {
let key = DispatchSpecificKey<Void>()
DispatchQueue.main.setSpecific(key: key, value: ())

let mainQueueCounter = Atomic(0)

let setter: (Int) -> Void = {
value = $0
mainQueueCounter.modify { $0 += DispatchQueue.getSpecific(key: key) != nil ? 1 : 0 }
}

target = BindingTarget(on: .main,
lifetime: lifetime,
setter: setter)

let scheduler: QueueScheduler
if #available(OSX 10.10, *) {
scheduler = QueueScheduler()
} else {
scheduler = QueueScheduler(queue: DispatchQueue(label: "com.reactivecocoa.ReactiveSwift.UnidirectionalBindingSpec"))
}

let property = MutableProperty(1)
target <~ property.producer
.start(on: scheduler)
.observe(on: scheduler)

expect(value).toEventually(equal(1))
expect(mainQueueCounter.value).toEventually(equal(1))

property.value = 2
expect(value).toEventually(equal(2))
expect(mainQueueCounter.value).toEventually(equal(2))
}
}
}
}

0 comments on commit 33f8a6d

Please sign in to comment.