Skip to content
This repository was archived by the owner on Dec 5, 2019. It is now read-only.

Commit 3a34dfe

Browse files
committed
Merge pull request ReactiveCocoa#1628 from ReactiveCocoa/improve-actions
Improve Action class, add CocoaAction wrapper, remove all automatic scheduling
2 parents aa936f8 + 632eada commit 3a34dfe

3 files changed

Lines changed: 345 additions & 174 deletions

File tree

ReactiveCocoa/Swift/Action.swift

Lines changed: 180 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -8,39 +8,57 @@
88

99
import LlamaKit
1010

11-
/// Represents a UI action that will perform some work when executed.
11+
/// Represents an action that will perform side effects or a transformation when
12+
/// executed with an input.
13+
///
14+
/// Actions must be executed serially. An attempt to execute the same action
15+
/// multiple times concurrently will fail.
1216
public final class Action<Input, Output> {
1317
private let executeClosure: Input -> ColdSignal<Output>
14-
private let scheduler: Scheduler
18+
private let valuesSink: SinkOf<Output>
19+
private let errorsSink: SinkOf<NSError>
1520

16-
private let _executing = ObservableProperty(false)
17-
private let _enabled = ObservableProperty(false)
18-
private let _values: SinkOf<Output>
19-
private let _errors: SinkOf<NSError>
21+
/// A signal of all values sent upon the signals returned from apply().
22+
public let values: HotSignal<Output>
23+
24+
/// A signal of all errors (except `RACAction.ActionNotEnabled` errors) sent
25+
/// upon the signals returned from apply().
26+
public let errors: HotSignal<NSError>
2027

2128
/// Whether the action is currently executing.
2229
///
23-
/// This will send the current value immediately, then all future values on
24-
/// the scheduler given at initialization time.
30+
/// This signal will send the current value immediately (if the action is
31+
/// alive), and complete when the action has deinitialized.
2532
public var executing: ColdSignal<Bool> {
26-
return _executing.values
33+
return executingProperty.values
2734
}
2835

2936
/// Whether the action is enabled.
3037
///
31-
/// This will send the current value immediately, then all future values on
32-
/// the scheduler given at initialization time.
38+
/// This signal will send the current value immediately (if the action is
39+
/// alive), and complete when the action has deinitialized.
3340
public var enabled: ColdSignal<Bool> {
34-
return _enabled.values
41+
return userEnabledProperty.values
42+
.combineLatestWith(executingProperty.values)
43+
.map(enabled)
3544
}
3645

37-
/// A signal of all values generated from future calls to execute(), sent on
38-
/// the scheduler given at initialization time.
39-
public let values: HotSignal<Output>
40-
41-
/// A signal of errors generated from future executions, sent on the
42-
/// scheduler given at initialization time.
43-
public let errors: HotSignal<NSError>
46+
// This queue serializes access to the properties below.
47+
//
48+
// Work which depends on consistent reads and writes of both properties
49+
// should be enqueued as a barrier block.
50+
//
51+
// If only one property is being used, the work can be enqueued as a normal
52+
// (non-barrier) block.
53+
private let propertyQueue = dispatch_queue_create("org.reactivecocoa.ReactiveCocoa.Action", DISPATCH_QUEUE_SERIAL)
54+
private let userEnabledProperty = ObservableProperty(false)
55+
private let executingProperty = ObservableProperty(false)
56+
57+
/// Whether the action should be enabled when `userEnabledProperty` and
58+
/// `executingProperty` have the given values.
59+
private func enabled(#userEnabled: Bool, executing: Bool) -> Bool {
60+
return userEnabled && !executing
61+
}
4462

4563
/// The file in which this action was defined, if known.
4664
internal let file: String?
@@ -55,75 +73,174 @@ public final class Action<Input, Output> {
5573
/// a ColdSignal for each execution.
5674
///
5775
/// Before `enabledIf` sends a value, the command will be disabled.
58-
public init(enabledIf: HotSignal<Bool>, serializedOnScheduler scheduler: Scheduler, _ execute: Input -> ColdSignal<Output>, file: String = __FILE__, line: Int = __LINE__, function: String = __FUNCTION__) {
76+
public init(enabledIf: HotSignal<Bool>, _ execute: Input -> ColdSignal<Output>, file: String = __FILE__, line: Int = __LINE__, function: String = __FUNCTION__) {
5977
self.file = file
6078
self.line = line
6179
self.function = function
62-
self.scheduler = scheduler
6380

64-
(values, _values) = HotSignal.pipe()
65-
(errors, _errors) = HotSignal.pipe()
81+
(values, valuesSink) = HotSignal.pipe()
82+
(errors, errorsSink) = HotSignal.pipe()
6683
executeClosure = execute
6784

68-
_enabled <~! ColdSignal.single(false)
69-
.concat(enabledIf
70-
.replay(1)
71-
.deliverOn(scheduler))
72-
.combineLatestWith(executing)
73-
.map { enabled, executing in enabled && !executing }
74-
}
75-
76-
/// Initializes an action that will deliver all events on the main thread.
77-
public convenience init(enabledIf: HotSignal<Bool>, _ execute: Input -> ColdSignal<Output>, file: String = __FILE__, line: Int = __LINE__, function: String = __FUNCTION__) {
78-
self.init(enabledIf: enabledIf, serializedOnScheduler: MainScheduler(), execute, file: file, line: line, function: function)
85+
userEnabledProperty <~ enabledIf
7986
}
8087

8188
/// Initializes an action that will always be enabled.
82-
public convenience init(serializedOnScheduler: Scheduler, _ execute: Input -> ColdSignal<Output>, file: String = __FILE__, line: Int = __LINE__, function: String = __FUNCTION__) {
89+
public convenience init(_ execute: Input -> ColdSignal<Output>, file: String = __FILE__, line: Int = __LINE__, function: String = __FUNCTION__) {
8390
let (enabled, enabledSink) = HotSignal<Bool>.pipe()
84-
self.init(enabledIf: enabled, serializedOnScheduler: serializedOnScheduler, execute, file: file, line: line, function: function)
91+
self.init(enabledIf: enabled, execute, file: file, line: line, function: function)
8592

8693
enabledSink.put(true)
8794
}
8895

89-
/// Initializes an action that will always be enabled, and deliver all
90-
/// events on the main thread.
91-
public convenience init(_ execute: Input -> ColdSignal<Output>, file: String = __FILE__, line: Int = __LINE__, function: String = __FUNCTION__) {
92-
self.init(serializedOnScheduler: MainScheduler(), execute, file: file, line: line, function: function)
93-
}
94-
95-
/// Creates a signal that will execute the action on the scheduler given at
96-
/// initialization time, with the given input, then forward the results.
96+
/// Creates a signal that will execute the action with the given input, then
97+
/// forward the results.
9798
///
9899
/// If the action is disabled when the returned signal is started, the
99-
/// signal will send an `NSError` corresponding to
100-
/// `RACError.ActionNotEnabled`, and no result will be sent along the action
101-
/// itself.
102-
public func execute(input: Input) -> ColdSignal<Output> {
100+
/// returned signal will send an `NSError` corresponding to
101+
/// `RACError.ActionNotEnabled`, and nothing will be sent upon `values` or
102+
/// `errors`.
103+
public func apply(input: Input) -> ColdSignal<Output> {
103104
return ColdSignal<Output>.lazy {
104-
let isEnabled = self.enabled.first().value()!
105-
if (!isEnabled) {
106-
return .error(RACError.ActionNotEnabled.error)
105+
var startedExecuting = false
106+
107+
dispatch_barrier_sync(self.propertyQueue) {
108+
if self.enabled(userEnabled: self.userEnabledProperty.value, executing: self.executingProperty.value) {
109+
self.executingProperty.value = true
110+
startedExecuting = true
107111
}
112+
}
108113

109-
return self.executeClosure(input)
110-
.deliverOn(self.scheduler)
111-
.on(started: {
112-
self._executing.value = true
113-
}, next: { value in
114-
self._values.put(value)
115-
}, error: { error in
116-
self._errors.put(error)
117-
}, disposed: {
118-
self._executing.value = false
119-
})
114+
if !startedExecuting {
115+
return .error(RACError.ActionNotEnabled.error)
120116
}
121-
.evaluateOn(scheduler)
117+
118+
return self.executeClosure(input)
119+
.on(next: { value in
120+
self.valuesSink.put(value)
121+
}, error: { error in
122+
self.errorsSink.put(error)
123+
}, disposed: {
124+
// This doesn't need to be a barrier because properties are
125+
// themselves serialized, and this operation therefore won't
126+
// conflict with other read-only or write-only operations.
127+
//
128+
// We still need to use the queue for mutual exclusion with
129+
// the enabledness check above.
130+
dispatch_async(self.propertyQueue) {
131+
self.executingProperty.value = false
132+
}
133+
})
134+
}
122135
}
123136
}
124137

125138
extension Action: DebugPrintable {
126139
public var debugDescription: String {
140+
let function = self.function ?? ""
141+
let file = self.file ?? ""
142+
let line = self.line?.description ?? ""
143+
127144
return "\(function).Action (\(file):\(line))"
128145
}
129146
}
147+
148+
/// Wraps an Action for use by a GUI control (such as `NSControl` or
149+
/// `UIControl`), with KVO, or with Cocoa Bindings.
150+
public final class CocoaAction: NSObject {
151+
/// Whether the action is enabled.
152+
///
153+
/// This property will only change on the main thread, and will generate a
154+
/// KVO notification for every change.
155+
public var enabled: Bool {
156+
return _enabled
157+
}
158+
159+
/// Whether the action is executing.
160+
///
161+
/// This property will only change on the main thread, and will generate a
162+
/// KVO notification for every change.
163+
public var executing: Bool {
164+
return _executing
165+
}
166+
167+
/// The selector that a caller should invoke upon the receiver in order to
168+
/// execute the action.
169+
public let selector: Selector = "execute:"
170+
171+
private let _execute: AnyObject? -> ()
172+
private var _enabled = false
173+
private var _executing = false
174+
private let disposable = CompositeDisposable()
175+
176+
private init(enabled: ColdSignal<Bool>, executing: ColdSignal<Bool>, execute: AnyObject? -> ()) {
177+
_execute = execute
178+
179+
super.init()
180+
181+
startSignalOnMainThread(enabled) { [weak self] value in
182+
self?.willChangeValueForKey("enabled")
183+
self?._enabled = value
184+
self?.didChangeValueForKey("enabled")
185+
}
186+
187+
startSignalOnMainThread(executing) { [weak self] value in
188+
self?.willChangeValueForKey("executing")
189+
self?._executing = value
190+
self?.didChangeValueForKey("executing")
191+
}
192+
}
193+
194+
/// Initializes a Cocoa action that will always invoke the given action with
195+
/// an input of ().
196+
public convenience init<Output>(_ action: Action<(), Output>) {
197+
self.init(enabled: action.enabled, executing: action.executing, execute: { object in
198+
action.apply(()).start()
199+
return
200+
})
201+
}
202+
203+
/// Initializes a Cocoa action that will always invoke the given action with
204+
/// an input of nil.
205+
public convenience init<Input: NilLiteralConvertible, Output>(_ action: Action<Input, Output>) {
206+
self.init(enabled: action.enabled, executing: action.executing, execute: { object in
207+
action.apply(nil).start()
208+
return
209+
})
210+
}
211+
212+
/// Initializes a Cocoa action that will invoke the given action with the
213+
/// object given to execute() if it can be downcast successfully, or nil
214+
/// otherwise.
215+
public convenience init<Input: AnyObject, Output>(_ action: Action<Input?, Output>) {
216+
self.init(enabled: action.enabled, executing: action.executing, execute: { object in
217+
action.apply(object as? Input).start()
218+
return
219+
})
220+
}
221+
222+
deinit {
223+
self.disposable.dispose()
224+
}
225+
226+
/// Starts the given signal, delivering its uniqued values to the main
227+
/// thread and executing the given closure for each one.
228+
private func startSignalOnMainThread<T: Equatable>(signal: ColdSignal<T>, next: T -> ()) {
229+
let signalDisposable = signal
230+
.skipRepeats(identity)
231+
.deliverOn(MainScheduler())
232+
.start(next: next)
233+
234+
self.disposable.addDisposable(signalDisposable)
235+
}
236+
237+
/// Attempts to execute the underlying action with the given input, subject
238+
/// to the behavior described by the initializer that was used.
239+
@IBAction public func execute(input: AnyObject?) {
240+
_execute(input)
241+
}
242+
243+
public override class func automaticallyNotifiesObserversForKey(key: String) -> Bool {
244+
return false
245+
}
246+
}

ReactiveCocoa/Swift/ObservableProperty.swift

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@ import Foundation
1010
import LlamaKit
1111

1212
/// A mutable property of type T that allows observation of its changes.
13+
///
14+
/// Instances of this class are thread-safe.
1315
public final class ObservableProperty<T> {
14-
private let queue = dispatch_queue_create("org.reactivecocoa.ReactiveCocoa.ObservableProperty", DISPATCH_QUEUE_SERIAL)
16+
private let queue = dispatch_queue_create("org.reactivecocoa.ReactiveCocoa.ObservableProperty", DISPATCH_QUEUE_CONCURRENT)
1517
private var sinks = Bag<SinkOf<Event<T>>>()
18+
private var _value: T
1619

1720
/// The file in which this property was defined, if known.
1821
internal let file: String?
@@ -28,10 +31,22 @@ public final class ObservableProperty<T> {
2831
/// Setting this to a new value will notify all sinks attached to
2932
/// `values`.
3033
public var value: T {
31-
didSet {
34+
get {
35+
var readValue: T?
36+
3237
dispatch_sync(queue) {
38+
readValue = self._value
39+
}
40+
41+
return readValue!
42+
}
43+
44+
set(value) {
45+
dispatch_barrier_sync(queue) {
46+
self._value = value
47+
3348
for sink in self.sinks {
34-
sink.put(.Next(Box(self.value)))
49+
sink.put(.Next(Box(value)))
3550
}
3651
}
3752
}
@@ -45,14 +60,14 @@ public final class ObservableProperty<T> {
4560
if let strongSelf = self {
4661
var token: RemovalToken?
4762

48-
dispatch_sync(strongSelf.queue) {
63+
dispatch_barrier_sync(strongSelf.queue) {
4964
token = strongSelf.sinks.insert(sink)
50-
sink.put(.Next(Box(strongSelf.value)))
65+
sink.put(.Next(Box(strongSelf._value)))
5166
}
5267

5368
disposable.addDisposable {
5469
if let strongSelf = self {
55-
dispatch_async(strongSelf.queue) {
70+
dispatch_barrier_async(strongSelf.queue) {
5671
strongSelf.sinks.removeValueForToken(token!)
5772
}
5873
}
@@ -64,14 +79,15 @@ public final class ObservableProperty<T> {
6479
}
6580

6681
public init(_ value: T, file: String = __FILE__, line: Int = __LINE__, function: String = __FUNCTION__) {
67-
self.value = value
82+
_value = value
83+
6884
self.file = file
6985
self.line = line
7086
self.function = function
7187
}
7288

7389
deinit {
74-
dispatch_sync(queue) {
90+
dispatch_barrier_sync(queue) {
7591
for sink in self.sinks {
7692
sink.put(.Completed)
7793
}
@@ -87,6 +103,10 @@ extension ObservableProperty: SinkType {
87103

88104
extension ObservableProperty: DebugPrintable {
89105
public var debugDescription: String {
106+
let function = self.function ?? ""
107+
let file = self.file ?? ""
108+
let line = self.line?.description ?? ""
109+
90110
return "\(function).ObservableProperty (\(file):\(line))"
91111
}
92112
}

0 commit comments

Comments
 (0)