RIBs/Classes/Workflow/Workflow.swift (99 lines of code) (raw):

// // Copyright (c) 2017. Uber Technologies // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // import RxSwift /// Defines the base class for a sequence of steps that execute a flow through the application RIB tree. /// /// At each step of a `Workflow` is a pair of value and actionable item. The value can be used to make logic decisions. /// The actionable item is invoked to perform logic for the step. Typically the actionable item is the `Interactor` of a /// RIB. /// /// A workflow should always start at the root of the tree. open class Workflow<ActionableItemType> { /// Called when the last step observable is completed. /// /// Subclasses should override this method if they want to execute logic at this point in the `Workflow` lifecycle. /// The default implementation does nothing. open func didComplete() { // No-op } /// Called when the `Workflow` is forked. /// /// Subclasses should override this method if they want to execute logic at this point in the `Workflow` lifecycle. /// The default implementation does nothing. open func didFork() { // No-op } /// Called when the last step observable is has error. /// /// Subclasses should override this method if they want to execute logic at this point in the `Workflow` lifecycle. /// The default implementation does nothing. open func didReceiveError(_ error: Error) { // No-op } /// Initializer. public init() {} /// Execute the given closure as the root step. /// /// - parameter onStep: The closure to execute for the root step. /// - returns: The next step. public final func onStep<NextActionableItemType, NextValueType>(_ onStep: @escaping (ActionableItemType) -> Observable<(NextActionableItemType, NextValueType)>) -> Step<ActionableItemType, NextActionableItemType, NextValueType> { return Step(workflow: self, observable: subject.asObservable().take(1)) .onStep { (actionableItem: ActionableItemType, _) in onStep(actionableItem) } } /// Subscribe and start the `Workflow` sequence. /// /// - parameter actionableItem: The initial actionable item for the first step. /// - returns: The disposable of this workflow. public final func subscribe(_ actionableItem: ActionableItemType) -> Disposable { guard compositeDisposable.count > 0 else { assertionFailure("Attempt to subscribe to \(self) before it is comitted.") return Disposables.create() } subject.onNext((actionableItem, ())) return compositeDisposable } // MARK: - Private private let subject = PublishSubject<(ActionableItemType, ())>() private var didInvokeComplete = false /// The composite disposable that contains all subscriptions including the original workflow /// as well as all the forked ones. fileprivate let compositeDisposable = CompositeDisposable() fileprivate func didCompleteIfNotYet() { // Since a workflow may be forked to produce multiple subscribed Rx chains, we should // ensure the didComplete method is only invoked once per Workflow instance. See `Step.commit` // on why the side-effects must be added at the end of the Rx chains. guard !didInvokeComplete else { return } didInvokeComplete = true didComplete() } } /// Defines a single step in a `Workflow`. /// /// A step may produce a next step with a new value and actionable item, eventually forming a sequence of `Workflow` /// steps. /// /// Steps are asynchronous by nature. open class Step<WorkflowActionableItemType, ActionableItemType, ValueType> { private let workflow: Workflow<WorkflowActionableItemType> private var observable: Observable<(ActionableItemType, ValueType)> fileprivate init(workflow: Workflow<WorkflowActionableItemType>, observable: Observable<(ActionableItemType, ValueType)>) { self.workflow = workflow self.observable = observable } /// Executes the given closure for this step. /// /// - parameter onStep: The closure to execute for the `Step`. /// - returns: The next step. public final func onStep<NextActionableItemType, NextValueType>(_ onStep: @escaping (ActionableItemType, ValueType) -> Observable<(NextActionableItemType, NextValueType)>) -> Step<WorkflowActionableItemType, NextActionableItemType, NextValueType> { let confinedNextStep = observable .flatMapLatest { (actionableItem, value) -> Observable<(Bool, ActionableItemType, ValueType)> in // We cannot use generic constraint here since Swift requires constraints be // satisfied by concrete types, preventing using protocol as actionable type. if let interactor = actionableItem as? Interactable { return interactor .isActiveStream .map({ (isActive: Bool) -> (Bool, ActionableItemType, ValueType) in (isActive, actionableItem, value) }) } else { return Observable.just((true, actionableItem, value)) } } .filter { (isActive: Bool, _, _) -> Bool in isActive } .take(1) .flatMapLatest { (_, actionableItem: ActionableItemType, value: ValueType) -> Observable<(NextActionableItemType, NextValueType)> in onStep(actionableItem, value) } .take(1) .share() return Step<WorkflowActionableItemType, NextActionableItemType, NextValueType>(workflow: workflow, observable: confinedNextStep) } /// Executes the given closure when the `Step` produces an error. /// /// - parameter onError: The closure to execute when an error occurs. /// - returns: This step. public final func onError(_ onError: @escaping ((Error) -> ())) -> Step<WorkflowActionableItemType, ActionableItemType, ValueType> { observable = observable.do(onError: onError) return self } /// Commit the steps of the `Workflow` sequence. /// /// - returns: The committed `Workflow`. @discardableResult public final func commit() -> Workflow<WorkflowActionableItemType> { // Side-effects must be chained at the last observable sequence, since errors and complete // events can be emitted by any observables on any steps of the workflow. let disposable = observable .do(onError: workflow.didReceiveError, onCompleted: workflow.didCompleteIfNotYet) .subscribe() _ = workflow.compositeDisposable.insert(disposable) return workflow } /// Convert the `Workflow` into an obseravble. /// /// - returns: The observable representation of this `Workflow`. public final func asObservable() -> Observable<(ActionableItemType, ValueType)> { return observable } } /// `Workflow` related obervable extensions. public extension ObservableType { /// Fork the step from this obervable. /// /// - parameter workflow: The workflow this step belongs to. /// - returns: The newly forked step in the workflow. `nil` if this observable does not conform to the required /// generic type of (ActionableItemType, ValueType). func fork<WorkflowActionableItemType, ActionableItemType, ValueType>(_ workflow: Workflow<WorkflowActionableItemType>) -> Step<WorkflowActionableItemType, ActionableItemType, ValueType>? { if let stepObservable = self as? Observable<(ActionableItemType, ValueType)> { workflow.didFork() return Step(workflow: workflow, observable: stepObservable) } return nil } } /// `Workflow` related `Disposable` extensions. public extension Disposable { /// Dispose the subscription when the given `Workflow` is disposed. /// /// When using this composition, the subscription closure may freely retain the workflow itself, since the /// subscription closure is disposed once the workflow is disposed, thus releasing the retain cycle before the /// `Workflow` needs to be deallocated. /// /// - note: This is the preferred method when trying to confine a subscription to the lifecycle of a `Workflow`. /// /// - parameter workflow: The workflow to dispose the subscription with. func disposeWith<ActionableItemType>(workflow: Workflow<ActionableItemType>) { _ = workflow.compositeDisposable.insert(self) } /// Dispose the subscription when the given `Workflow` is disposed. /// /// When using this composition, the subscription closure may freely retain the workflow itself, since the /// subscription closure is disposed once the workflow is disposed, thus releasing the retain cycle before the /// `Workflow` needs to be deallocated. /// /// - note: This is the preferred method when trying to confine a subscription to the lifecycle of a `Workflow`. /// /// - parameter workflow: The workflow to dispose the subscription with. @available(*, deprecated, renamed: "disposeWith(workflow:)") func disposeWith<ActionableItemType>(worflow: Workflow<ActionableItemType>) { disposeWith(workflow: worflow) } }