RIBs/Classes/Router.swift (98 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 /// The lifecycle stages of a router scope. public enum RouterLifecycle { /// Router did load. case didLoad } /// The scope of a `Router`, defining various lifecycles of a `Router`. public protocol RouterScope: AnyObject { /// An observable that emits values when the router scope reaches its corresponding life-cycle stages. This /// observable completes when the router scope is deallocated. var lifecycle: Observable<RouterLifecycle> { get } } /// The base protocol for all routers. public protocol Routing: RouterScope { // 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 child router, the mocked child router first needs to conform to the // custom subclass routing protocol, and also this base protocol to allow the `Router` implementation to execute // base class logic without error. /// The base interactable associated with this `Router`. var interactable: Interactable { get } /// The list of children routers of this `Router`. var children: [Routing] { get } /// Loads the `Router`. /// /// - note: This method is internally used by the framework. Application code should never /// invoke this method explicitly. func load() // We cannot declare the attach/detach child methods to take in concrete `Router` instances, // since during unit testing, we need to use mocked child routers. /// Attaches the given router as a child. /// /// - parameter child: The child router to attach. func attachChild(_ child: Routing) /// Detaches the given router from the tree. /// /// - parameter child: The child router to detach. func detachChild(_ child: Routing) } /// The base class of all routers that does not own view controllers, representing application states. /// /// A router acts on inputs from its corresponding interactor, to manipulate application state, forming a tree of /// routers. A router may obtain a view controller through constructor injection to manipulate view controller tree. /// The DI structure guarantees that the injected view controller must be from one of this router's ancestors. /// Router drives the lifecycle of its owned `Interactor`. /// /// Routers should always use helper builders to instantiate children routers. open class Router<InteractorType>: Routing { /// The corresponding `Interactor` owned by this `Router`. public let interactor: InteractorType /// The base `Interactable` associated with this `Router`. public let interactable: Interactable /// The list of children `Router`s of this `Router`. public final var children: [Routing] = [] /// The observable that emits values when the router scope reaches its corresponding life-cycle stages. /// /// This observable completes when the router scope is deallocated. public final var lifecycle: Observable<RouterLifecycle> { return lifecycleSubject.asObservable() } /// Initializer. /// /// - parameter interactor: The corresponding `Interactor` of this `Router`. public init(interactor: InteractorType) { self.interactor = interactor guard let interactable = interactor as? Interactable else { fatalError("\(interactor) should conform to \(Interactable.self)") } self.interactable = interactable } /// Loads the `Router`. /// /// - note: This method is internally used by the framework. Application code should never invoke this method /// explicitly. public final func load() { guard !didLoadFlag else { return } didLoadFlag = true internalDidLoad() didLoad() } /// Called when the router has finished loading. /// /// This method is invoked only once. Subclasses should override this method to perform one time setup logic, /// such as attaching immutable children. The default implementation does nothing. open func didLoad() { // No-op } // We cannot declare the attach/detach child methods to take in concrete `Router` instances, // since during unit testing, we need to use mocked child routers. /// Attaches the given router as a child. /// /// - parameter child: The child `Router` to attach. public final func attachChild(_ child: Routing) { assert(!(children.contains { $0 === child }), "Attempt to attach child: \(child), which is already attached to \(self).") children.append(child) // Activate child first before loading. Router usually attaches immutable children in didLoad. // We need to make sure the RIB is activated before letting it attach immutable children. child.interactable.activate() child.load() } /// Detaches the given `Router` from the tree. /// /// - parameter child: The child `Router` to detach. public final func detachChild(_ child: Routing) { child.interactable.deactivate() children.removeElementByReference(child) } // MARK: - Internal let deinitDisposable = CompositeDisposable() func internalDidLoad() { bindSubtreeActiveState() lifecycleSubject.onNext(.didLoad) } // MARK: - Private private let lifecycleSubject = PublishSubject<RouterLifecycle>() private var didLoadFlag: Bool = false private func bindSubtreeActiveState() { let disposable = interactable.isActiveStream // Do not retain self here to guarantee execution. Retaining self will cause the dispose bag // to never be disposed, thus self is never deallocated. Also cannot just store the disposable // and call dispose(), since we want to keep the subscription alive until deallocation, in // case the router is re-attached. Using weak does require the router to be retained until its // interactor is deactivated. .subscribe(onNext: { [weak self] (isActive: Bool) in // When interactor becomes active, we are attached to parent, otherwise we are detached. self?.setSubtreeActive(isActive) }) _ = deinitDisposable.insert(disposable) } private func setSubtreeActive(_ active: Bool) { if active { iterateSubtree(self) { router in if !router.interactable.isActive { router.interactable.activate() } } } else { iterateSubtree(self) { router in if router.interactable.isActive { router.interactable.deactivate() } } } } private func iterateSubtree(_ root: Routing, closure: (_ node: Routing) -> ()) { closure(root) for child in root.children { iterateSubtree(child, closure: closure) } } private func detachAllChildren() { for child in children { detachChild(child) } } deinit { interactable.deactivate() if !children.isEmpty { detachAllChildren() } lifecycleSubject.onCompleted() deinitDisposable.dispose() LeakDetector.instance.expectDeallocate(object: interactable) } }