Skip to content

Commit

Permalink
Refactored Action.
Browse files Browse the repository at this point in the history
  • Loading branch information
andersio committed Nov 18, 2016
1 parent fea3dc6 commit 4c9a18b
Showing 1 changed file with 27 additions and 61 deletions.
88 changes: 27 additions & 61 deletions Sources/Action.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,14 @@ import enum Result.NoError
public final class Action<Input, Output, Error: Swift.Error> {
private let deinitToken: Lifetime.Token

private let isUserEnabled: (Any) -> Bool
private let executeClosure: (_ state: Any, _ input: Input) -> SignalProducer<Output, Error>
private let eventsObserver: Signal<Event<Output, Error>, NoError>.Observer
private let disabledErrorsObserver: Signal<(), NoError>.Observer

private let _isExecuting = MutableProperty<Bool>(false)
private let state: Property<Any>

/// The lifetime of the Action.
public let lifetime: Lifetime

Expand Down Expand Up @@ -47,13 +51,13 @@ public final class Action<Input, Output, Error: Swift.Error> {
public let completed: Signal<(), NoError>

/// Whether the action is currently executing.
public let isExecuting: Property<Bool>
public var isExecuting: Property<Bool> {
return Property(_isExecuting)
}

/// Whether the action is currently enabled.
public let isEnabled: Property<Bool>

private let state: MutableProperty<ActionState>

/// Initializes an action that will be conditionally enabled, and creates a
/// SignalProducer for each input.
///
Expand All @@ -78,28 +82,17 @@ public final class Action<Input, Output, Error: Swift.Error> {
deinitToken = Lifetime.Token()
lifetime = Lifetime(deinitToken)

state = property.map { $0 as Any }
executeClosure = { state, input in execute(state as! State.Value, input) }
isUserEnabled = { isEnabled($0 as! State.Value) }
self.isEnabled = state.combineLatest(with: _isExecuting).map { !$1 && isEnabled($0 as! State.Value) }

(events, eventsObserver) = Signal<Event<Output, Error>, 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(isExecuting: false, 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
Expand Down Expand Up @@ -143,55 +136,28 @@ public final class Action<Input, Output, Error: Swift.Error> {
/// - input: A value that will be passed to the closure creating the signal
/// producer.
public func apply(_ input: Input) -> SignalProducer<Output, ActionError<Error>> {
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
return state.producer
.take(first: 1)
.flatMap(.concat) { state -> SignalProducer<Output, ActionError<Error>> in
let shouldStart: Bool = self._isExecuting.modify { isExecuting in
if isExecuting || !self.isUserEnabled(state) {
return false
}

isExecuting = true
return true
}
}

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)
if !shouldStart {
defer { self.disabledErrorsObserver.send(value: ()) }
return SignalProducer(error: .disabled)
}
}

disposable += {
self.state.modify {
$0.isExecuting = false
}
return self.executeClosure(state, input)
.on(event: self.eventsObserver.send(value:),
disposed: { self._isExecuting.value = false })
.mapError(ActionError.producerFailed)
}
}
}
}

private struct ActionState {
var isExecuting: Bool
var value: Any
private let userEnabled: (Any) -> Bool

init(isExecuting: Bool, value: Any, isEnabled: @escaping (Any) -> Bool) {
self.isExecuting = isExecuting
self.value = value
self.userEnabled = isEnabled
}

/// Whether the action should be enabled for the given combination of user
/// enabledness and executing status.
fileprivate var isEnabled: Bool {
return userEnabled(value) && !isExecuting
}
}

Expand Down

0 comments on commit 4c9a18b

Please sign in to comment.