diff --git a/Package.swift b/Package.swift index c4a9143fb..7951d1245 100644 --- a/Package.swift +++ b/Package.swift @@ -11,6 +11,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/ReactiveX/RxSwift", from: "6.0.0"), + .package(url: "https://github.com/ReactiveX/RxSwift", from: "6.0.0"), ], targets: [ .target( diff --git a/ios/RIBs.xcodeproj/project.pbxproj b/ios/RIBs.xcodeproj/project.pbxproj index b5ae108cb..24eeb4d23 100644 --- a/ios/RIBs.xcodeproj/project.pbxproj +++ b/ios/RIBs.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 48; + objectVersion = 52; objects = { /* Begin PBXBuildFile section */ @@ -22,6 +22,11 @@ 4131773D1F8EF98A005F08F0 /* Foundation+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4131773C1F8EF98A005F08F0 /* Foundation+Extensions.swift */; }; 418C175C1F97F19F003C03F7 /* Component.swift in Sources */ = {isa = PBXBuildFile; fileRef = 418C17571F97DB2E003C03F7 /* Component.swift */; }; 418C175D1F97F1A1003C03F7 /* Dependency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 418C17561F97DB2E003C03F7 /* Dependency.swift */; }; + 4E03148926448F3900088902 /* AutoDetachRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E03148826448F3900088902 /* AutoDetachRegistry.swift */; }; + 4E03148E2644927500088902 /* RxSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 4E03148D2644927500088902 /* RxSwift */; }; + 4E0314902644927500088902 /* RxRelay in Frameworks */ = {isa = PBXBuildFile; productRef = 4E03148F2644927500088902 /* RxRelay */; }; + 4E0314922644927500088902 /* RxCocoa in Frameworks */ = {isa = PBXBuildFile; productRef = 4E0314912644927500088902 /* RxCocoa */; }; + 4E0314942644927500088902 /* RxBlocking in Frameworks */ = {isa = PBXBuildFile; productRef = 4E0314932644927500088902 /* RxBlocking */; }; 8B9882EC1F86E1CF00ABE009 /* RIBs.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B9882E21F86E1CF00ABE009 /* RIBs.framework */; }; 8B9882F31F86E1CF00ABE009 /* RIBs.h in Headers */ = {isa = PBXBuildFile; fileRef = 8B9882E51F86E1CF00ABE009 /* RIBs.h */; settings = {ATTRIBUTES = (Public, ); }; }; AF90B40A1FBA14DB00920384 /* RouterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF90B4091FBA14DB00920384 /* RouterTests.swift */; }; @@ -61,6 +66,7 @@ 4131773C1F8EF98A005F08F0 /* Foundation+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Foundation+Extensions.swift"; sourceTree = ""; }; 418C17561F97DB2E003C03F7 /* Dependency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dependency.swift; sourceTree = ""; }; 418C17571F97DB2E003C03F7 /* Component.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Component.swift; sourceTree = ""; }; + 4E03148826448F3900088902 /* AutoDetachRegistry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoDetachRegistry.swift; sourceTree = ""; }; 8B9882E21F86E1CF00ABE009 /* RIBs.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = RIBs.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 8B9882E51F86E1CF00ABE009 /* RIBs.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RIBs.h; sourceTree = ""; }; 8B9882E61F86E1CF00ABE009 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -83,7 +89,11 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 4E03148E2644927500088902 /* RxSwift in Frameworks */, + 4E0314942644927500088902 /* RxBlocking in Frameworks */, BF5FC0F122808377004235F1 /* RxRelay.framework in Frameworks */, + 4E0314922644927500088902 /* RxCocoa in Frameworks */, + 4E0314902644927500088902 /* RxRelay in Frameworks */, AF9966B21FC40D7E00CAEAA2 /* RxSwift.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -115,6 +125,7 @@ 413177361F8EF70A005F08F0 /* LeakDetector */, 4131772A1F8EEFF0005F08F0 /* Worker */, 413177201F8EEFEF005F08F0 /* Workflow */, + 4E03148826448F3900088902 /* AutoDetachRegistry.swift */, ); path = Classes; sourceTree = ""; @@ -275,6 +286,12 @@ dependencies = ( ); name = RIBs; + packageProductDependencies = ( + 4E03148D2644927500088902 /* RxSwift */, + 4E03148F2644927500088902 /* RxRelay */, + 4E0314912644927500088902 /* RxCocoa */, + 4E0314932644927500088902 /* RxBlocking */, + ); productName = RIBs; productReference = 8B9882E21F86E1CF00ABE009 /* RIBs.framework */; productType = "com.apple.product-type.framework"; @@ -329,6 +346,9 @@ Base, ); mainGroup = 8B9882D81F86E1CF00ABE009; + packageReferences = ( + 4E03148C2644927500088902 /* XCRemoteSwiftPackageReference "RxSwift" */, + ); productRefGroup = 8B9882E31F86E1CF00ABE009 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -396,6 +416,7 @@ 4131772C1F8EF5FF005F08F0 /* Builder.swift in Sources */, 413177331F8EF5FF005F08F0 /* ViewControllable.swift in Sources */, 413177311F8EF5FF005F08F0 /* Router.swift in Sources */, + 4E03148926448F3900088902 /* AutoDetachRegistry.swift in Sources */, 413177301F8EF5FF005F08F0 /* Presenter.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -536,7 +557,8 @@ IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; @@ -561,7 +583,11 @@ INFOPLIST_FILE = RIBs/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 9.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = com.uber.RIBs; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -588,7 +614,11 @@ INFOPLIST_FILE = RIBs/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 9.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = com.uber.RIBs; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -607,7 +637,11 @@ "$(PRODUCTS_DIR)../Carthage/Build/iOS/", ); INFOPLIST_FILE = RIBsTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = com.uber.RIBsTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -625,7 +659,11 @@ "$(PRODUCTS_DIR)../Carthage/Build/iOS/", ); INFOPLIST_FILE = RIBsTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = com.uber.RIBsTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -664,6 +702,40 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 4E03148C2644927500088902 /* XCRemoteSwiftPackageReference "RxSwift" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/ReactiveX/RxSwift"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 6.1.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 4E03148D2644927500088902 /* RxSwift */ = { + isa = XCSwiftPackageProductDependency; + package = 4E03148C2644927500088902 /* XCRemoteSwiftPackageReference "RxSwift" */; + productName = RxSwift; + }; + 4E03148F2644927500088902 /* RxRelay */ = { + isa = XCSwiftPackageProductDependency; + package = 4E03148C2644927500088902 /* XCRemoteSwiftPackageReference "RxSwift" */; + productName = RxRelay; + }; + 4E0314912644927500088902 /* RxCocoa */ = { + isa = XCSwiftPackageProductDependency; + package = 4E03148C2644927500088902 /* XCRemoteSwiftPackageReference "RxSwift" */; + productName = RxCocoa; + }; + 4E0314932644927500088902 /* RxBlocking */ = { + isa = XCSwiftPackageProductDependency; + package = 4E03148C2644927500088902 /* XCRemoteSwiftPackageReference "RxSwift" */; + productName = RxBlocking; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 8B9882D91F86E1CF00ABE009 /* Project object */; } diff --git a/ios/RIBs/Classes/AutoDetachRegistry.swift b/ios/RIBs/Classes/AutoDetachRegistry.swift new file mode 100644 index 000000000..791261c73 --- /dev/null +++ b/ios/RIBs/Classes/AutoDetachRegistry.swift @@ -0,0 +1,167 @@ +// +// Copyright © Uber Technologies, Inc. All rights reserved. +// + +import RxSwift +import RxCocoa +import UIKit + +public enum ViewControllerLifecycleEvent: Int { + + /// The view controller's view did load. + case viewDidLoad + + /// The view controller's view will appear. + case viewWillAppear + + /// The view controller's view did appear. + case viewDidAppear + + /// The view controller's view will disappear. + case viewWillDisappear + + /// The view controller's view did disappear. + case viewDidDisappear +} + +/// The scope of a view controller providing lifecycle observables. +/// @CreateMock +public protocol ViewControllerScope: AnyObject { + + /// The observable of this view controller's lifecycle events. This observable completes + /// when this controller is deallocated, and only emits new values when it is different + /// from the last value. + /// - note: Subscription to this stream always immediately returning the most recent + /// event if there is one. + var lifecycle: Observable { get } +} + + +/// Registry to handle auto detaching viewable router. +/// We will detach child when child view controller is not on screen and not owned by any view controller. +/// @CreateMock +public protocol AutoDetachRegistering: AnyObject { + /// Attach child to parent router and register for auto detaching. + /// + /// - parameter child: The child viewable router. + /// - parameter parent: The parent router. + func attachViewableChild(_ child: ViewableRouting, with parent: Routing) + + /// Attach child to parent router and register for auto detaching. + /// + /// - parameter child: The child viewable router. + /// - parameter parent: The parent router. + /// - parameter detachedHandler: The handler will be called when the child is detached. + func attachViewableChild(_ child: ViewableRouting, with parent: Routing, detachedHandler: (() -> ())?) +} + +public final class AutoDetachRegistry: AutoDetachRegistering, AutoDetachHandlerListener { + + public init() {} + + public func attachViewableChild(_ child: ViewableRouting, with parent: Routing) { + attachViewableChild(child, with: parent, detachedHandler: nil) + } + + public func attachViewableChild(_ child: ViewableRouting, with parent: Routing, detachedHandler: (() -> ())?) { + parent.attachChild(child) + let handler = AutoDetachHandler(child: child, + parent: parent, + detachedHandler: detachedHandler) + handler.listener = self + handlers.append(handler) + } + + // MARK: - AutoDetachHandlerListener + + func didDetach(_ handler: AutoDetachHandler) { + handlers.removeElementByReference(handler) + } + + // MARK: - Private + + private var handlers: [AutoDetachHandler] = [] +} + +protocol AutoDetachHandlerListener: AnyObject { + func didDetach(_ handler: AutoDetachHandler) +} + +final class AutoDetachHandler { + + weak var listener: AutoDetachHandlerListener? + + init(child: ViewableRouting, + parent: Routing, + detachedHandler: (() -> ())?) { + self.child = child + self.parent = parent + self.detachedHandler = detachedHandler + subscribeChildLifecycle() + } + + func detachIfNeeded() { + guard let child = child else { + detachedHandler?() + listener?.didDetach(self) + return + } + + guard shouldDetach(child.viewControllable) else { return } + + parent?.detachChild(child) + detachedHandler?() + listener?.didDetach(self) + } + + // MARK: - Private + + private weak var parent: Routing? + private weak var child: ViewableRouting? + private let detachedHandler: (() -> ())? + private let disposable = DisposeBag() + private let dismissSubject = PublishSubject<()>() + + private func subscribeChildLifecycle() { + guard let vc = child?.viewControllable as? ViewControllerScope, + // NEAL: skip NoRxCocoaUsages on the next line because of lifecycle observation. + let parentEvent = child?.viewControllable.uiviewController.rx.observeWeakly(UIViewController.self, "parentViewController") else { + assertionFailure("Auto detach only supports ViewControllerScope") + return + } + let didDisappear = vc.lifecycle + .filter { event in event == .viewDidDisappear } + .map { _ in } + let parentDidMove = Observable.zip(parentEvent, parentEvent.skip(1)) + .filter { pre, current in pre != nil && current == nil } + .map { _ in } + let dismiss = dismissSubject + .asObservable() + .debounce(.milliseconds(500), scheduler: MainScheduler.instance) + + Observable.merge(didDisappear, parentDidMove, dismiss) + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] in + self?.detachIfNeeded() + }) + .disposed(by: disposable) + } + + private func shouldDetach(_ viewController: ViewControllable) -> Bool { + let vc = viewController.uiviewController + // if the view controller is being dismissed, check later. + let navigationController = vc.navigationController + if (navigationController?.isBeingDismissed ?? false) || vc.isBeingDismissed { + dismissSubject.onNext(()) + return false + } + + let isVcGone = vc.parent == nil && vc.presentingViewController == nil && vc.view.window == nil + guard let navController = navigationController else { + return isVcGone + } + + // if the navigation controller is gone, we should dismiss the vc as well. + return isVcGone || (navController.parent == nil && navController.presentingViewController == nil && vc.view.window == nil) + } +}