Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Autodetachregistry #446

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
84 changes: 78 additions & 6 deletions ios/RIBs.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 48;
objectVersion = 52;
objects = {

/* Begin PBXBuildFile section */
Expand All @@ -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 */; };
Expand Down Expand Up @@ -61,6 +66,7 @@
4131773C1F8EF98A005F08F0 /* Foundation+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Foundation+Extensions.swift"; sourceTree = "<group>"; };
418C17561F97DB2E003C03F7 /* Dependency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dependency.swift; sourceTree = "<group>"; };
418C17571F97DB2E003C03F7 /* Component.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Component.swift; sourceTree = "<group>"; };
4E03148826448F3900088902 /* AutoDetachRegistry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoDetachRegistry.swift; sourceTree = "<group>"; };
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 = "<group>"; };
8B9882E61F86E1CF00ABE009 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
Expand All @@ -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;
Expand Down Expand Up @@ -115,6 +125,7 @@
413177361F8EF70A005F08F0 /* LeakDetector */,
4131772A1F8EEFF0005F08F0 /* Worker */,
413177201F8EEFEF005F08F0 /* Workflow */,
4E03148826448F3900088902 /* AutoDetachRegistry.swift */,
);
path = Classes;
sourceTree = "<group>";
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -329,6 +346,9 @@
Base,
);
mainGroup = 8B9882D81F86E1CF00ABE009;
packageReferences = (
4E03148C2644927500088902 /* XCRemoteSwiftPackageReference "RxSwift" */,
);
productRefGroup = 8B9882E31F86E1CF00ABE009 /* Products */;
projectDirPath = "";
projectRoot = "";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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";
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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 */;
}
167 changes: 167 additions & 0 deletions ios/RIBs/Classes/AutoDetachRegistry.swift
Original file line number Diff line number Diff line change
@@ -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<ViewControllerLifecycleEvent> { 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)
}
}