import Dispatch 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 deinitToken: Lifetime.Token private let executeClosure: (_ state: Any, _ input: Input) -> SignalProducer private let eventsObserver: Signal, NoError>.Observer private let disabledErrorsObserver: Signal<(), NoError>.Observer /// The lifetime of the Action. public let lifetime: Lifetime /// 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() except `ActionError.disabled`. 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() except `ActionError.disabled`. 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() except `ActionError.disabled`. public let errors: Signal /// A signal which is triggered by `ActionError.disabled`. public let disabledErrors: Signal<(), NoError> /// A signal of all completed events generated from applications of the action. /// /// In other words, this will send completed events from every signal generated /// by each SignalProducer returned from apply(). public let completed: Signal<(), NoError> /// Whether the action is currently executing. public let isExecuting: Property /// Whether the action is currently enabled. public let isEnabled: Property private let state: MutableProperty /// Initializes an action that will be conditionally enabled based on the /// value of `state`. Creates a `SignalProducer` for each input and the /// current value of `state`. /// /// - note: `Action` guarantees that changes to `state` are observed in a /// thread-safe way. Thus, the value passed to `isEnabled` will /// always be identical to the value passed to `execute`, for each /// application of the action. /// /// - note: This initializer should only be used if you need to provide /// custom input can also influence whether the action is enabled. /// The various convenience initializers should cover most use cases. /// /// - parameters: /// - state: A property that provides the current state of the action /// whenever `apply()` is called. /// - enabledIf: A predicate that, given the current value of `state`, /// returns whether the action should be enabled. /// - execute: A closure that returns the `SignalProducer` returned by /// calling `apply(Input)` on the action, optionally using /// the current value of `state`. public init(state property: State, enabledIf isEnabled: @escaping (State.Value) -> Bool, _ execute: @escaping (State.Value, Input) -> SignalProducer) { deinitToken = Lifetime.Token() lifetime = Lifetime(deinitToken) // Retain the `property` for the created `Action`. lifetime.ended.observeCompleted { _ = property } executeClosure = { state, input in execute(state as! State.Value, input) } (events, eventsObserver) = Signal, NoError>.pipe() (disabledErrors, disabledErrorsObserver) = Signal<(), NoError>.pipe() values = events.map { $0.value }.skipNil() errors = events.map { $0.error }.skipNil() completed = events.filter { $0.isCompleted }.map { _ in } let initial = ActionState(value: property.value, isEnabled: { isEnabled($0 as! State.Value) }) state = MutableProperty(initial) property.signal .take(during: state.lifetime) .observeValues { [weak state] newValue in state?.modify { $0.value = newValue } } self.isEnabled = state.map { $0.isEnabled } self.isExecuting = state.map { $0.isExecuting } } /// 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 convenience init(enabledIf property: P, _ execute: @escaping (Input) -> SignalProducer) where P.Value == Bool { self.init(state: property, enabledIf: { $0 }) { _, input in execute(input) } } /// 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: @escaping (Input) -> SignalProducer) { self.init(enabledIf: Property(value: true), execute) } deinit { eventsObserver.sendCompleted() disabledErrorsObserver.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.disabled`, /// 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. public func apply(_ input: Input) -> SignalProducer> { return SignalProducer { observer, disposable in let startingState = self.state.modify { state -> Any? in if state.isEnabled { state.isExecuting = true return state.value } else { return nil } } guard let state = startingState else { observer.send(error: .disabled) self.disabledErrorsObserver.send(value: ()) return } self.executeClosure(state, input).startWithSignal { signal, signalDisposable in disposable += signalDisposable signal.observe { event in observer.action(event.mapError(ActionError.producerFailed)) self.eventsObserver.send(value: event) } } disposable += { self.state.modify { $0.isExecuting = false } } } } } private struct ActionState { var isExecuting: Bool = false var value: Any { didSet { userEnabled = userEnabledClosure(value) } } private var userEnabled: Bool private let userEnabledClosure: (Any) -> Bool init(value: Any, isEnabled: @escaping (Any) -> Bool) { self.value = value self.userEnabled = isEnabled(value) self.userEnabledClosure = isEnabled } /// Whether the action should be enabled for the given combination of user /// enabledness and executing status. fileprivate var isEnabled: Bool { return userEnabled && !isExecuting } } /// A protocol used to constraint `Action` initializers. public protocol ActionProtocol: BindingTargetProtocol { /// 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: Swift.Error /// Initializes an action that will be conditionally enabled based on the /// value of `state`. Creates a `SignalProducer` for each input and the /// current value of `state`. /// /// - note: `Action` guarantees that changes to `state` are observed in a /// thread-safe way. Thus, the value passed to `isEnabled` will /// always be identical to the value passed to `execute`, for each /// application of the action. /// /// - note: This initializer should only be used if you need to provide /// custom input can also influence whether the action is enabled. /// The various convenience initializers should cover most use cases. /// /// - parameters: /// - state: A property that provides the current state of the action /// whenever `apply()` is called. /// - enabledIf: A predicate that, given the current value of `state`, /// returns whether the action should be enabled. /// - execute: A closure that returns the `SignalProducer` returned by /// calling `apply(Input)` on the action, optionally using /// the current value of `state`. init(state property: State, enabledIf isEnabled: @escaping (State.Value) -> Bool, _ execute: @escaping (State.Value, Input) -> SignalProducer) /// Whether the action is currently enabled. var isEnabled: Property { 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.disabled`, /// 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 ActionProtocol { public func consume(_ value: Input) { apply(value).start() } } extension Action: ActionProtocol { public var action: Action { return self } } extension ActionProtocol where Input == Void { /// Initializes an action that uses an `Optional` property for its input, /// and is disabled whenever the input is `nil`. When executed, a `SignalProducer` /// is created with the current value of the input. /// /// - parameters: /// - input: An `Optional` property whose current value is used as input /// whenever the action is executed. The action is disabled /// whenever the value is `nil`. /// - execute: A closure to return a new `SignalProducer` based on the /// current value of `input`. public init(input: P, _ execute: @escaping (T) -> SignalProducer) where P.Value == T? { self.init(state: input, enabledIf: { $0 != nil }) { input, _ in execute(input!) } } /// Initializes an action that uses a property for its input. When executed, /// a `SignalProducer` is created with the current value of the input. /// /// - parameters: /// - input: A property whose current value is used as input /// whenever the action is executed. /// - execute: A closure to return a new `SignalProducer` based on the /// current value of `input`. public init(input: P, _ execute: @escaping (T) -> SignalProducer) where P.Value == T { self.init(input: input.map(Optional.some), execute) } } /// 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: Swift.Error { /// The producer returned from apply() was started while the Action was /// disabled. case disabled /// The producer returned from apply() sent the given error. case producerFailed(Error) } public func == (lhs: ActionError, rhs: ActionError) -> Bool { switch (lhs, rhs) { case (.disabled, .disabled): return true case let (.producerFailed(left), .producerFailed(right)): return left == right default: return false } }