Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# master
*Please add new entries at the top.*

1. In Swift 3.2 or later, you may create `BindingTarget` for a key path of a specific object. (#440, kudos to @andersio)

# 2.0.0-alpha.2
1. In Swift 3.2 or later, you can use `map()` with the new Smart Key Paths. (#435, kudos to @sharplet)

Expand Down
26 changes: 26 additions & 0 deletions Sources/UnidirectionalBinding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,30 @@ public struct BindingTarget<Value>: BindingTargetProvider {
}
self.init(lifetime: lifetime, action: setter)
}

#if swift(>=3.2)
// `Lifetime` is required on these overloads. RAC would provide convenience overloads
// for these with `lifetime(of:)`.

/// Creates a binding target.
///
/// - parameters:
/// - lifetime: The expected lifetime of any bindings towards `self`.
/// - object: The object to consume values.
/// - keyPath: The key path of the object that consumes values.
public init<Object: AnyObject>(lifetime: Lifetime, object: Object, keyPath: ReferenceWritableKeyPath<Object, Value>) {
self.init(lifetime: lifetime) { [weak object] in object?[keyPath: keyPath] = $0 }
}

/// Creates a binding target which consumes values on the specified scheduler.
///
/// - parameters:
/// - scheduler: The scheduler on which the `setter` consumes the values.
/// - lifetime: The expected lifetime of any bindings towards `self`.
/// - object: The object to consume values.
/// - keyPath: The key path of the object that consumes values.
public init<Object: AnyObject>(on scheduler: Scheduler, lifetime: Lifetime, object: Object, keyPath: ReferenceWritableKeyPath<Object, Value>) {
self.init(on: scheduler, lifetime: lifetime) { [weak object] in object?[keyPath: keyPath] = $0 }
}
#endif
}
126 changes: 88 additions & 38 deletions Tests/ReactiveSwiftTests/UnidirectionalBindingSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,87 +5,135 @@ import Nimble
import Quick
@testable import ReactiveSwift

private class Object {
var value: Int = 0
}

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

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

describe("non-optional target") {
it("should pass through the lifetime") {
expect(target.lifetime).to(beIdenticalTo(lifetime))
describe("closure binding target") {
var target: BindingTarget<Int>!
var optionalTarget: BindingTarget<Int?>!
var value: Int?

beforeEach {
target = BindingTarget(lifetime: lifetime, action: { value = $0 })
optionalTarget = BindingTarget(lifetime: lifetime, action: { value = $0 })
value = nil
}

it("should trigger the supplied setter") {
expect(value).to(beNil())
describe("non-optional target") {
it("should pass through the lifetime") {
expect(target.lifetime).to(beIdenticalTo(lifetime))
}

target.action(1)
expect(value) == 1
it("should trigger the supplied setter") {
expect(value).to(beNil())

target.action(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 accept bindings from properties") {
expect(value).to(beNil())
describe("optional target") {
it("should pass through the lifetime") {
expect(optionalTarget.lifetime).to(beIdenticalTo(lifetime))
}

let property = MutableProperty(1)
target <~ property
expect(value) == 1
it("should trigger the supplied setter") {
expect(value).to(beNil())

property.value = 2
expect(value) == 2
optionalTarget.action(1)
expect(value) == 1
}

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

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

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

describe("optional target") {
#if swift(>=3.2)
describe("key path binding target") {
var target: BindingTarget<Int>!
var object: Object!

beforeEach {
object = Object()
target = BindingTarget(lifetime: lifetime, object: object, keyPath: \.value)
}

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

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

optionalTarget.action(1)
expect(value) == 1
target.action(1)
expect(object.value) == 1
}

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

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

property.value = 2
expect(value) == 2
expect(object.value) == 2
}
}
#endif

it("should not deadlock on the same queue") {
target = BindingTarget(on: UIScheduler(),
lifetime: lifetime,
action: { value = $0 })
var value: Int?

let target = BindingTarget(on: UIScheduler(),
lifetime: lifetime,
action: { value = $0 })

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

it("should not deadlock on the main thread even if the context was switched to a different queue") {
var value: Int?

let queue = DispatchQueue(label: #file)

target = BindingTarget(on: UIScheduler(),
lifetime: lifetime,
action: { value = $0 })
let target = BindingTarget(on: UIScheduler(),
lifetime: lifetime,
action: { value = $0 })

let property = MutableProperty(1)

Expand All @@ -97,6 +145,8 @@ class UnidirectionalBindingSpec: QuickSpec {
}

it("should not deadlock even if the value is originated from the same queue indirectly") {
var value: Int?

let key = DispatchSpecificKey<Void>()
DispatchQueue.main.setSpecific(key: key, value: ())

Expand All @@ -107,9 +157,9 @@ class UnidirectionalBindingSpec: QuickSpec {
mainQueueCounter.modify { $0 += DispatchQueue.getSpecific(key: key) != nil ? 1 : 0 }
}

target = BindingTarget(on: UIScheduler(),
lifetime: lifetime,
action: setter)
let target = BindingTarget(on: UIScheduler(),
lifetime: lifetime,
action: setter)

let scheduler: QueueScheduler
if #available(OSX 10.10, *) {
Expand Down