RIBs/Classes/Interactor.swift (80 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 Foundation
import RxSwift
import UIKit
/// Protocol defining the activeness of an interactor's scope.
public protocol InteractorScope: AnyObject {
// The following properties must be declared in the base protocol, since `Router` internally invokes these methods.
// In order to unit test router with a mock interactor, the mocked interactor first needs to conform to the custom
// subclass interactor protocol, and also this base protocol to allow the `Router` implementation to execute base
// class logic without error.
/// Indicates if the interactor is active.
var isActive: Bool { get }
/// The lifecycle of this interactor.
///
/// - note: Subscription to this stream always immediately returns the last event. This stream terminates after
/// the interactor is deallocated.
var isActiveStream: Observable<Bool> { get }
}
/// The base protocol for all interactors.
public protocol Interactable: InteractorScope {
// The following methods must be declared in the base protocol, since `Router` internally invokes these methods.
// In order to unit test router with a mock interactor, the mocked interactor first needs to conform to the custom
// subclass interactor protocol, and also this base protocol to allow the `Router` implementation to execute base
// class logic without error.
/// Activate this interactor.
///
/// - note: This method is internally invoked by the corresponding router. Application code should never explicitly
/// invoke this method.
func activate()
/// Deactivate this interactor.
///
/// - note: This method is internally invoked by the corresponding router. Application code should never explicitly
/// invoke this method.
func deactivate()
}
/// An `Interactor` defines a unit of business logic that corresponds to a router unit.
///
/// An `Interactor` has a lifecycle driven by its owner router. When the corresponding router is attached to its
/// parent, its interactor becomes active. And when the router is detached from its parent, its `Interactor` resigns
/// active.
///
/// An `Interactor` should only perform its business logic when it's currently active.
open class Interactor: Interactable {
/// Indicates if the interactor is active.
public final var isActive: Bool {
do {
return try isActiveSubject.value()
} catch {
return false
}
}
/// A stream notifying on the lifecycle of this interactor.
public final var isActiveStream: Observable<Bool> {
return isActiveSubject.asObservable().distinctUntilChanged()
}
/// Initializer.
public init() {
// No-op
}
/// Activate the `Interactor`.
///
/// - note: This method is internally invoked by the corresponding router. Application code should never explicitly
/// invoke this method.
public final func activate() {
guard !isActive else {
return
}
activenessDisposable = CompositeDisposable()
isActiveSubject.onNext(true)
didBecomeActive()
}
/// The interactor did become active.
///
/// - note: This method is driven by the attachment of this interactor's owner router. Subclasses should override
/// this method to setup subscriptions and initial states.
open func didBecomeActive() {
// No-op
}
/// Deactivate this `Interactor`.
///
/// - note: This method is internally invoked by the corresponding router. Application code should never explicitly
/// invoke this method.
public final func deactivate() {
guard isActive else {
return
}
willResignActive()
activenessDisposable?.dispose()
activenessDisposable = nil
isActiveSubject.onNext(false)
}
/// Called when the `Interactor` will resign the active state.
///
/// This method is driven by the detachment of this interactor's owner router. Subclasses should override this
/// method to cleanup any resources and states of the `Interactor`. The default implementation does nothing.
open func willResignActive() {
// No-op
}
// MARK: - Private
private let isActiveSubject = BehaviorSubject<Bool>(value: false)
fileprivate var activenessDisposable: CompositeDisposable?
deinit {
if isActive {
deactivate()
}
isActiveSubject.onCompleted()
}
}
/// Interactor related `Observable` extensions.
public extension ObservableType {
/// Confines the observable's subscription to the given interactor scope. The subscription is only triggered
/// after the interactor scope is active and before the interactor scope resigns active. This composition
/// delays the subscription but does not dispose the subscription, when the interactor scope becomes inactive.
///
/// - note: This method should only be used for subscriptions outside of an `Interactor`, for cases where a
/// piece of logic is only executed when the bound interactor scope is active.
///
/// - note: Only the latest value from this observable is emitted. Values emitted when the interactor is not
/// active, are ignored.
///
/// - parameter interactorScope: The interactor scope whose activeness this observable is confined to.
/// - returns: The `Observable` confined to this interactor's activeness lifecycle.
func confineTo(_ interactorScope: InteractorScope) -> Observable<Element> {
return Observable
.combineLatest(interactorScope.isActiveStream, self) { isActive, value in
(isActive, value)
}
.filter { isActive, value in
isActive
}
.map { isActive, value in
value
}
}
}
/// Interactor related `Disposable` extensions.
public extension Disposable {
/// Disposes the subscription based on the lifecycle of the given `Interactor`. The subscription is disposed
/// when the interactor is deactivated.
///
/// - note: This is the preferred method when trying to confine a subscription to the lifecycle of an
/// `Interactor`.
///
/// When using this composition, the subscription closure may freely retain the interactor itself, since the
/// subscription closure is disposed once the interactor is deactivated, thus releasing the retain cycle before
/// the interactor needs to be deallocated.
///
/// If the given interactor is inactive at the time this method is invoked, the subscription is immediately
/// terminated.
///
/// - parameter interactor: The interactor to dispose the subscription based on.
@discardableResult
func disposeOnDeactivate(interactor: Interactor) -> Disposable {
if let activenessDisposable = interactor.activenessDisposable {
_ = activenessDisposable.insert(self)
} else {
dispose()
print("Subscription immediately terminated, since \(interactor) is inactive.")
}
return self
}
}