forked from ReactiveCocoa/ReactiveCocoa
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Action.swift
254 lines (210 loc) · 8.13 KB
/
Action.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
/// Represents an action that will do some work when executed with a value of
/// type `Input`, then return zero or more values of type `Output` and/or error
/// out with an error of type `Error`. If no errors should be possible, NoError
/// can be specified for the `Error` parameter.
///
/// Actions enforce serial execution. Any attempt to execute an action multiple
/// times concurrently will return an error.
public final class Action<Input, Output, Error: ErrorType> {
private let executeClosure: Input -> SignalProducer<Output, Error>
private let eventsObserver: Signal<Event<Output, Error>, NoError>.Observer
/// A signal of all events generated from applications of the Action.
///
/// In other words, this will send every `Event` from every signal generated
/// by each SignalProducer returned from apply().
public let events: Signal<Event<Output, Error>, NoError>
/// A signal of all values generated from applications of the Action.
///
/// In other words, this will send every value from every signal generated
/// by each SignalProducer returned from apply().
public let values: Signal<Output, NoError>
/// A signal of all errors generated from applications of the Action.
///
/// In other words, this will send errors from every signal generated by
/// each SignalProducer returned from apply().
public let errors: Signal<Error, NoError>
/// Whether the action is currently executing.
public var executing: PropertyOf<Bool> {
return PropertyOf(_executing)
}
private let _executing: MutableProperty<Bool> = MutableProperty(false)
/// Whether the action is currently enabled.
public var enabled: PropertyOf<Bool> {
return PropertyOf(_enabled)
}
private let _enabled: MutableProperty<Bool> = MutableProperty(false)
/// Whether the instantiator of this action wants it to be enabled.
private let userEnabled: PropertyOf<Bool>
/// Lazy creation and storage of a UI bindable `CocoaAction`. The default behavior
/// force casts the AnyObject? input to match the action's `Input` type. This makes
/// it unsafe for use when the action is parameterized for something like `Void`
/// input. In those cases, explicitly assign a value to this property that transforms
/// the input to suit your needs.
public lazy var unsafeCocoaAction: CocoaAction = { _ in
CocoaAction(self) { $0 as! Input }
}()
/// This queue is used for read-modify-write operations on the `_executing`
/// property.
private let executingQueue = dispatch_queue_create("org.reactivecocoa.ReactiveCocoa.Action.executingQueue", DISPATCH_QUEUE_SERIAL)
/// Whether the action should be enabled for the given combination of user
/// enabledness and executing status.
private static func shouldBeEnabled(#userEnabled: Bool, executing: Bool) -> Bool {
return userEnabled && !executing
}
/// Initializes an action that will be conditionally enabled, and create a
/// SignalProducer for each input.
public init<P: PropertyType where P.Value == Bool>(enabledIf: P, _ execute: Input -> SignalProducer<Output, Error>) {
executeClosure = execute
userEnabled = PropertyOf(enabledIf)
(events, eventsObserver) = Signal<Event<Output, Error>, NoError>.pipe()
values = events |> map { $0.value } |> ignoreNil
errors = events |> map { $0.error } |> ignoreNil
_enabled <~ enabledIf.producer
|> combineLatestWith(executing.producer)
|> map(Action.shouldBeEnabled)
}
/// Initializes an action that will be enabled by default, and create a
/// SignalProducer for each input.
public convenience init(_ execute: Input -> SignalProducer<Output, Error>) {
self.init(enabledIf: ConstantProperty(true), execute)
}
deinit {
sendCompleted(eventsObserver)
}
/// Creates a SignalProducer that, when started, will execute the action
/// with the given input, then forward the results upon the produced Signal.
///
/// If the action is disabled when the returned SignalProducer is started,
/// the produced signal will send `ActionError.NotEnabled`, and nothing will
/// be sent upon `values` or `errors` for that particular signal.
public func apply(input: Input) -> SignalProducer<Output, ActionError<Error>> {
return SignalProducer { observer, disposable in
var startedExecuting = false
dispatch_sync(self.executingQueue) {
if self._enabled.value {
self._executing.value = true
startedExecuting = true
}
}
if !startedExecuting {
sendError(observer, .NotEnabled)
return
}
self.executeClosure(input).startWithSignal { signal, signalDisposable in
disposable.addDisposable(signalDisposable)
signal.observe(Signal.Observer { event in
observer.put(event.mapError { .ProducerError($0) })
sendNext(self.eventsObserver, event)
})
}
disposable.addDisposable {
self._executing.value = false
}
}
}
}
/// Wraps an Action for use by a GUI control (such as `NSControl` or
/// `UIControl`), with KVO, or with Cocoa Bindings.
public final class CocoaAction: NSObject {
/// The selector that a caller should invoke upon a CocoaAction in order to
/// execute it.
public static let selector: Selector = "execute:"
/// Whether the action is enabled.
///
/// This property will only change on the main thread, and will generate a
/// KVO notification for every change.
public var enabled: Bool {
return _enabled
}
/// Whether the action is executing.
///
/// This property will only change on the main thread, and will generate a
/// KVO notification for every change.
public var executing: Bool {
return _executing
}
private var _enabled = false
private var _executing = false
private let _execute: AnyObject? -> ()
private let disposable = CompositeDisposable()
/// Initializes a Cocoa action that will invoke the given Action by
/// transforming the object given to execute().
public init<Input, Output, Error>(_ action: Action<Input, Output, Error>, _ inputTransform: AnyObject? -> Input) {
_execute = { input in
let producer = action.apply(inputTransform(input))
producer.start()
}
super.init()
disposable += action.enabled.producer
|> observeOn(UIScheduler())
|> start(next: { [weak self] value in
self?.willChangeValueForKey("enabled")
self?._enabled = value
self?.didChangeValueForKey("enabled")
})
disposable += action.executing.producer
|> observeOn(UIScheduler())
|> start(next: { [weak self] value in
self?.willChangeValueForKey("executing")
self?._executing = value
self?.didChangeValueForKey("executing")
})
}
/// Initializes a Cocoa action that will invoke the given Action by
/// always providing the given input.
public convenience init<Input, Output, Error>(_ action: Action<Input, Output, Error>, input: Input) {
self.init(action, { _ in input })
}
deinit {
disposable.dispose()
}
/// Attempts to execute the underlying action with the given input, subject
/// to the behavior described by the initializer that was used.
@IBAction public func execute(input: AnyObject?) {
_execute(input)
}
public override class func automaticallyNotifiesObserversForKey(key: String) -> Bool {
return false
}
}
/// The type of error that can occur from Action.apply, where `E` is the type of
/// error that can be generated by the specific Action instance.
public enum ActionError<E: ErrorType> {
/// The producer returned from apply() was started while the Action was
/// disabled.
case NotEnabled
/// The producer returned from apply() sent the given error.
case ProducerError(E)
}
extension ActionError: ErrorType {
public var nsError: NSError {
switch self {
case .NotEnabled:
return NSError(domain: "org.reactivecocoa.ReactiveCocoa.Action", code: 1, userInfo: [
NSLocalizedDescriptionKey: self.description
])
case let .ProducerError(error):
return error.nsError
}
}
}
extension ActionError: Printable {
public var description: String {
switch self {
case .NotEnabled:
return "Action executed while disabled"
case let .ProducerError(error):
return toString(error)
}
}
}
public func == <E: Equatable>(lhs: ActionError<E>, rhs: ActionError<E>) -> Bool {
switch (lhs, rhs) {
case (.NotEnabled, .NotEnabled):
return true
case let (.ProducerError(left), .ProducerError(right)):
return left == right
default:
return false
}
}