88
99import 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.
1216public 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
125138extension 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+ }
0 commit comments