import Foundation
import enum Result.NoError
/// 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 fail
/// with an error of type `Error`. If no failure 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 {
private let executeClosure: Input -> SignalProducer
private let eventsObserver: Signal, 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, 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
/// 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
/// Whether the action is currently executing.
public var executing: AnyProperty {
return AnyProperty(_executing)
}
private let _executing: MutableProperty = MutableProperty(false)
/// Whether the action is currently enabled.
public var enabled: AnyProperty {
return AnyProperty(_enabled)
}
private let _enabled: MutableProperty = MutableProperty(false)
/// Whether the instantiator of this action wants it to be enabled.
private let userEnabled: AnyProperty
/// 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 userEnabled: Bool, executing: Bool) -> Bool {
return userEnabled && !executing
}
/// Initializes an action that will be conditionally enabled, and creates a
/// SignalProducer for each input.
///
/// - parameters:
/// - enabledIf: Boolean property that shows whether the action is
/// enabled.
/// - execute: A closure that returns the signal producer returned by
/// calling `apply(Input)` on the action.
public init(enabledIf: P, _ execute: Input -> SignalProducer) {
executeClosure = execute
userEnabled = AnyProperty(enabledIf)
(events, eventsObserver) = Signal, 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 creates a
/// SignalProducer for each input.
///
/// - parameters:
/// - execute: A closure that returns the signal producer returned by
/// calling `apply(Input)` on the action.
public convenience init(_ execute: Input -> SignalProducer) {
self.init(enabledIf: ConstantProperty(true), execute)
}
deinit {
eventsObserver.sendCompleted()
}
/// Creates a SignalProducer that, when started, will execute the action
/// with the given input, then forward the results upon the produced Signal.
///
/// - note: 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.
///
/// - parameters:
/// - input: A value that will be passed to the closure creating the signal
/// producer.
@warn_unused_result(message="Did you forget to call `start` on the producer?")
public func apply(input: Input) -> SignalProducer> {
return SignalProducer { observer, disposable in
var startedExecuting = false
dispatch_sync(self.executingQueue) {
if self._enabled.value {
self._executing.value = true
startedExecuting = true
}
}
if !startedExecuting {
observer.sendFailed(.NotEnabled)
return
}
self.executeClosure(input).startWithSignal { signal, signalDisposable in
disposable.addDisposable(signalDisposable)
signal.observe { event in
observer.action(event.mapError(ActionError.ProducerError))
self.eventsObserver.sendNext(event)
}
}
disposable += {
self._executing.value = false
}
}
}
}
public protocol ActionType {
/// The type of argument to apply the action to.
associatedtype Input
/// The type of values returned by the action.
associatedtype Output
/// The type of error when the action fails. If errors aren't possible then
/// `NoError` can be used.
associatedtype Error: ErrorType
/// Whether the action is currently enabled.
var enabled: AnyProperty { get }
/// Extracts an action from the receiver.
var action: Action { get }
/// Creates a SignalProducer that, when started, will execute the action
/// with the given input, then forward the results upon the produced Signal.
///
/// - note: 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.
///
/// - parameters:
/// - input: A value that will be passed to the closure creating the signal
/// producer.
func apply(input: Input) -> SignalProducer>
}
extension Action: ActionType {
public var action: Action {
return self
}
}
/// The type of error that can occur from Action.apply, where `Error` is the
/// type of error that can be generated by the specific Action instance.
public enum ActionError: 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(Error)
}
public func == (lhs: ActionError, rhs: ActionError) -> Bool {
switch (lhs, rhs) {
case (.NotEnabled, .NotEnabled):
return true
case let (.ProducerError(left), .ProducerError(right)):
return left == right
default:
return false
}
}