@@ -12,7 +12,7 @@ import enum Result.NoError
1212public final class Action < Input, Output, Error: Swift . Error > {
1313 private let deinitToken : Lifetime . Token
1414
15- private let executeClosure : ( Input ) -> SignalProducer < Output , Error >
15+ private let executeClosure : ( _ state : Any , _ input : Input ) -> SignalProducer < Output , Error >
1616 private let eventsObserver : Signal < Event < Output , Error > , NoError > . Observer
1717 private let disabledErrorsObserver : Signal < ( ) , NoError > . Observer
1818
@@ -49,43 +49,37 @@ public final class Action<Input, Output, Error: Swift.Error> {
4949 /// Whether the action is currently executing.
5050 public let isExecuting : Property < Bool >
5151
52- private let _isExecuting : MutableProperty < Bool > = MutableProperty ( false )
53-
5452 /// Whether the action is currently enabled.
55- public var isEnabled : Property < Bool >
56-
57- private let _isEnabled : MutableProperty < Bool > = MutableProperty ( false )
58-
59- /// Whether the instantiator of this action wants it to be enabled.
60- private let isUserEnabled : Property < Bool >
61-
62- /// This queue is used for read-modify-write operations on the `_executing`
63- /// property.
64- private let executingQueue = DispatchQueue (
65- label: " org.reactivecocoa.ReactiveSwift.Action.executingQueue " ,
66- attributes: [ ]
67- )
53+ public let isEnabled : Property < Bool >
6854
69- /// Whether the action should be enabled for the given combination of user
70- /// enabledness and executing status.
71- private static func shouldBeEnabled( userEnabled: Bool , executing: Bool ) -> Bool {
72- return userEnabled && !executing
73- }
55+ private let state : MutableProperty < ActionState >
7456
75- /// Initializes an action that will be conditionally enabled, and creates a
76- /// SignalProducer for each input.
57+ /// Initializes an action that will be conditionally enabled based on the
58+ /// value of `state`. Creates a `SignalProducer` for each input and the
59+ /// current value of `state`.
60+ ///
61+ /// - note: `Action` guarantees that changes to `state` are observed in a
62+ /// thread-safe way. Thus, the value passed to `isEnabled` will
63+ /// always be identical to the value passed to `execute`, for each
64+ /// application of the action.
65+ ///
66+ /// - note: This initializer should only be used if you need to provide
67+ /// custom input can also influence whether the action is enabled.
68+ /// The various convenience initializers should cover most use cases.
7769 ///
7870 /// - parameters:
79- /// - enabledIf: Boolean property that shows whether the action is
80- /// enabled.
81- /// - execute: A closure that returns the signal producer returned by
82- /// calling `apply(Input)` on the action.
83- public init < P: PropertyProtocol > ( enabledIf property: P , _ execute: @escaping ( Input ) -> SignalProducer < Output , Error > ) where P. Value == Bool {
71+ /// - state: A property that provides the current state of the action
72+ /// whenever `apply()` is called.
73+ /// - enabledIf: A predicate that, given the current value of `state`,
74+ /// returns whether the action should be enabled.
75+ /// - execute: A closure that returns the `SignalProducer` returned by
76+ /// calling `apply(Input)` on the action, optionally using
77+ /// the current value of `state`.
78+ public init < State: PropertyProtocol > ( state property: State , enabledIf isEnabled: @escaping ( State . Value ) -> Bool , _ execute: @escaping ( State . Value , Input ) -> SignalProducer < Output , Error > ) {
8479 deinitToken = Lifetime . Token ( )
8580 lifetime = Lifetime ( deinitToken)
8681
87- executeClosure = execute
88- isUserEnabled = Property ( property)
82+ executeClosure = { state, input in execute ( state as! State . Value , input) }
8983
9084 ( events, eventsObserver) = Signal < Event < Output , Error > , NoError > . pipe ( )
9185 ( disabledErrors, disabledErrorsObserver) = Signal < ( ) , NoError > . pipe ( )
@@ -94,12 +88,33 @@ public final class Action<Input, Output, Error: Swift.Error> {
9488 errors = events. map { $0. error } . skipNil ( )
9589 completed = events. filter { $0. isCompleted } . map { _ in }
9690
97- isEnabled = Property ( _isEnabled )
98- isExecuting = Property ( _isExecuting )
91+ let initial = ActionState ( value : property . value , isEnabled : { isEnabled ( $0 as! State . Value ) } )
92+ state = MutableProperty ( initial )
9993
100- _isEnabled <~ property. producer
101- . combineLatest ( with: isExecuting. producer)
102- . map ( Action . shouldBeEnabled)
94+ property. signal
95+ . take ( during: state. lifetime)
96+ . observeValues { [ weak state] newValue in
97+ state? . modify {
98+ $0. value = newValue
99+ }
100+ }
101+
102+ self . isEnabled = state. map { $0. isEnabled }
103+ self . isExecuting = state. map { $0. isExecuting }
104+ }
105+
106+ /// Initializes an action that will be conditionally enabled, and creates a
107+ /// `SignalProducer` for each input.
108+ ///
109+ /// - parameters:
110+ /// - enabledIf: Boolean property that shows whether the action is
111+ /// enabled.
112+ /// - execute: A closure that returns the signal producer returned by
113+ /// calling `apply(Input)` on the action.
114+ public convenience init < P: PropertyProtocol > ( enabledIf property: P , _ execute: @escaping ( Input ) -> SignalProducer < Output , Error > ) where P. Value == Bool {
115+ self . init ( state: property, enabledIf: { $0 } ) { _, input in
116+ execute ( input)
117+ }
103118 }
104119
105120 /// Initializes an action that will be enabled by default, and creates a
@@ -130,22 +145,22 @@ public final class Action<Input, Output, Error: Swift.Error> {
130145 /// producer.
131146 public func apply( _ input: Input ) -> SignalProducer < Output , ActionError < Error > > {
132147 return SignalProducer { observer, disposable in
133- var startedExecuting = false
134-
135- self . executingQueue . sync {
136- if self . _isEnabled . value {
137- self . _isExecuting . value = true
138- startedExecuting = true
148+ let startingState = self . state . modify { state -> Any ? in
149+ if state . isEnabled {
150+ state . isExecuting = true
151+ return state . value
152+ } else {
153+ return nil
139154 }
140155 }
141156
142- if !startedExecuting {
157+ guard let state = startingState else {
143158 observer. send ( error: . disabled)
144159 self . disabledErrorsObserver. send ( value: ( ) )
145160 return
146161 }
147162
148- self . executeClosure ( input) . startWithSignal { signal, signalDisposable in
163+ self . executeClosure ( state , input) . startWithSignal { signal, signalDisposable in
149164 disposable += signalDisposable
150165
151166 signal. observe { event in
@@ -155,12 +170,39 @@ public final class Action<Input, Output, Error: Swift.Error> {
155170 }
156171
157172 disposable += {
158- self . _isExecuting. value = false
173+ self . state. modify {
174+ $0. isExecuting = false
175+ }
159176 }
160177 }
161178 }
162179}
163180
181+ private struct ActionState {
182+ var isExecuting : Bool = false
183+
184+ var value : Any {
185+ didSet {
186+ userEnabled = userEnabledClosure ( value)
187+ }
188+ }
189+
190+ private var userEnabled : Bool
191+ private let userEnabledClosure : ( Any ) -> Bool
192+
193+ init ( value: Any , isEnabled: @escaping ( Any ) -> Bool ) {
194+ self . value = value
195+ self . userEnabled = isEnabled ( value)
196+ self . userEnabledClosure = isEnabled
197+ }
198+
199+ /// Whether the action should be enabled for the given combination of user
200+ /// enabledness and executing status.
201+ fileprivate var isEnabled : Bool {
202+ return userEnabled && !isExecuting
203+ }
204+ }
205+
164206public protocol ActionProtocol : BindingTargetProtocol {
165207 /// The type of argument to apply the action to.
166208 associatedtype Input
@@ -170,6 +212,29 @@ public protocol ActionProtocol: BindingTargetProtocol {
170212 /// `NoError` can be used.
171213 associatedtype Error : Swift . Error
172214
215+ /// Initializes an action that will be conditionally enabled based on the
216+ /// value of `state`. Creates a `SignalProducer` for each input and the
217+ /// current value of `state`.
218+ ///
219+ /// - note: `Action` guarantees that changes to `state` are observed in a
220+ /// thread-safe way. Thus, the value passed to `isEnabled` will
221+ /// always be identical to the value passed to `execute`, for each
222+ /// application of the action.
223+ ///
224+ /// - note: This initializer should only be used if you need to provide
225+ /// custom input can also influence whether the action is enabled.
226+ /// The various convenience initializers should cover most use cases.
227+ ///
228+ /// - parameters:
229+ /// - state: A property that provides the current state of the action
230+ /// whenever `apply()` is called.
231+ /// - enabledIf: A predicate that, given the current value of `state`,
232+ /// returns whether the action should be enabled.
233+ /// - execute: A closure that returns the `SignalProducer` returned by
234+ /// calling `apply(Input)` on the action, optionally using
235+ /// the current value of `state`.
236+ init < State: PropertyProtocol > ( state property: State , enabledIf isEnabled: @escaping ( State . Value ) -> Bool , _ execute: @escaping ( State . Value , Input ) -> SignalProducer < Output , Error > )
237+
173238 /// Whether the action is currently enabled.
174239 var isEnabled : Property < Bool > { get }
175240
@@ -202,6 +267,36 @@ extension Action: ActionProtocol {
202267 }
203268}
204269
270+ extension ActionProtocol where Input == Void {
271+ /// Initializes an action that uses an `Optional` property for its input,
272+ /// and is disabled whenever the input is `nil`. When executed, a `SignalProducer`
273+ /// is created with the current value of the input.
274+ ///
275+ /// - parameters:
276+ /// - input: An `Optional` property whose current value is used as input
277+ /// whenever the action is executed. The action is disabled
278+ /// whenever the value is `nil`.
279+ /// - execute: A closure to return a new `SignalProducer` based on the
280+ /// current value of `input`.
281+ public init < P: PropertyProtocol , T> ( input: P , _ execute: @escaping ( T ) -> SignalProducer < Output , Error > ) where P. Value == T ? {
282+ self . init ( state: input, enabledIf: { $0 != nil } ) { input, _ in
283+ execute ( input!)
284+ }
285+ }
286+
287+ /// Initializes an action that uses a property for its input. When executed,
288+ /// a `SignalProducer` is created with the current value of the input.
289+ ///
290+ /// - parameters:
291+ /// - input: A property whose current value is used as input
292+ /// whenever the action is executed.
293+ /// - execute: A closure to return a new `SignalProducer` based on the
294+ /// current value of `input`.
295+ public init < P: PropertyProtocol , T> ( input: P , _ execute: @escaping ( T ) -> SignalProducer < Output , Error > ) where P. Value == T {
296+ self . init ( input: input. map ( Optional . some) , execute)
297+ }
298+ }
299+
205300/// The type of error that can occur from Action.apply, where `Error` is the
206301/// type of error that can be generated by the specific Action instance.
207302public enum ActionError < Error: Swift . Error > : Swift . Error {
0 commit comments