From 14512c33147caccb594c6d066c0e2c3a4d271717 Mon Sep 17 00:00:00 2001 From: Anton Siliuk Date: Fri, 26 Mar 2021 18:15:30 +0100 Subject: [PATCH 1/4] Fix Flow leak when has .one() step and is only Flow emits rxDismissed --- RxFlow/FlowCoordinator.swift | 6 ++-- RxFlowTests/FlowCoordinatorTests.swift | 47 ++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/RxFlow/FlowCoordinator.swift b/RxFlow/FlowCoordinator.swift index d00b55f..4f9203b 100644 --- a/RxFlow/FlowCoordinator.swift +++ b/RxFlow/FlowCoordinator.swift @@ -46,13 +46,11 @@ public final class FlowCoordinator: NSObject { // listen for the internal steps relay that aggregates the flow's Stepper's steps and // the FlowContributors's Stepper's steps self.stepsRelay - .take(until: allowStepWhenDismissed ? .empty() : flow.rxDismissed.asObservable()) .do(onDispose: { [weak self] in self?.childFlowCoordinators.removeAll() self?.parentFlowCoordinator?.childFlowCoordinators.removeValue(forKey: self?.identifier ?? "") }) - .asSignal(onErrorJustReturn: NoneStep()) - .flatMapLatest { flow.adapt(step: $0).asSignal(onErrorJustReturn: NoneStep()) } + .flatMapLatest { flow.adapt(step: $0) } .do(onNext: { [weak self] in self?.willNavigateRelay.accept((flow, $0)) }) .map { return (flowContributors: flow.navigate(to: $0), step: $0) } .do(onNext: { [weak self] in self?.didNavigateRelay.accept((flow, $0.step)) }) @@ -98,6 +96,8 @@ public final class FlowCoordinator: NSObject { .flatMap { [weak self] in self?.steps(from: $0, within: flow, allowStepWhenDismissed: allowStepWhenDismissed) ?? Signal.empty() } + .take(until: allowStepWhenDismissed ? .empty() : flow.rxDismissed.asObservable()) + .asSignal(onErrorJustReturn: NoneStep()) .emit(to: self.stepsRelay) .disposed(by: self.disposeBag) diff --git a/RxFlowTests/FlowCoordinatorTests.swift b/RxFlowTests/FlowCoordinatorTests.swift index 661dd6e..5d7b5cb 100644 --- a/RxFlowTests/FlowCoordinatorTests.swift +++ b/RxFlowTests/FlowCoordinatorTests.swift @@ -207,6 +207,28 @@ final class TestDismissedFlow: Flow { } } +final class TestLeakingFlow: Flow { + var root: Presentable = UIViewController() + + var rxDismissed: Single { rxDismissedRelay.take(1).asSingle() } + let rxDismissedRelay = PublishRelay() + + func navigate(to step: Step) -> FlowContributors { + guard let step = step as? TestSteps else { return .none } + + switch step { + case .one: + let flowContributor = FlowContributor.contribute( + withNextPresentable: UIViewController(), + withNextStepper: DefaultStepper() + ) + return .one(flowContributor: flowContributor) + default: + return .none + } + } +} + final class FlowCoordinatorTests: XCTestCase { func testCoordinateWithOneStepper() { @@ -320,6 +342,31 @@ final class FlowCoordinatorTests: XCTestCase { let recordedSteps = try? dismissedFlow.recordedSteps.take(3).toBlocking().toArray() XCTAssertEqual(recordedSteps, [.one, .two, .three]) } + + func testFlowIsNotLeakingWhenHasOneStep() throws { + weak var leakingFlowReference: TestLeakingFlow? + let exp = expectation(description: "Flow when ready") + let flowCoordinator = FlowCoordinator() + + withExtendedLifetime(TestLeakingFlow()) { leakingFlow in + leakingFlowReference = leakingFlow + + flowCoordinator.coordinate(flow: leakingFlow, + with: OneStepper(withSingleStep: TestSteps.one)) + + Flows.use(leakingFlow, when: .created) { (_) in + exp.fulfill() + } + } + + waitForExpectations(timeout: 1) + + XCTAssertNotNil(leakingFlowReference) + + try XCTUnwrap(leakingFlowReference).rxDismissedRelay.accept(Void()) + + XCTAssertNil(leakingFlowReference) + } } #endif From 0b1a16ae2fc6b683f6b3345bc0178c90cda72475 Mon Sep 17 00:00:00 2001 From: Thibault Wittemberg Date: Sun, 11 Apr 2021 17:54:41 -0400 Subject: [PATCH 2/4] project: bump xcode proj to 2.12.1 --- RxFlow.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RxFlow.xcodeproj/project.pbxproj b/RxFlow.xcodeproj/project.pbxproj index 6c7a5e1..8934fb3 100644 --- a/RxFlow.xcodeproj/project.pbxproj +++ b/RxFlow.xcodeproj/project.pbxproj @@ -604,7 +604,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 2.11.0; + MARKETING_VERSION = 2.12.1; PRODUCT_BUNDLE_IDENTIFIER = io.warpfactor.RxFlow; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -639,7 +639,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 2.11.0; + MARKETING_VERSION = 2.12.1; PRODUCT_BUNDLE_IDENTIFIER = io.warpfactor.RxFlow; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; From 0ea5e1c17b784a21cc762eef5a54c7f5a4cc6718 Mon Sep 17 00:00:00 2001 From: Thibault Wittemberg Date: Sun, 11 Apr 2021 17:55:41 -0400 Subject: [PATCH 3/4] project: bump podspec to 2.12.1 --- RxFlow.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RxFlow.podspec b/RxFlow.podspec index 74042ad..5e6453f 100644 --- a/RxFlow.podspec +++ b/RxFlow.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "RxFlow" - s.version = "2.12.0" + s.version = "2.12.1" s.swift_version = '5.3' s.summary = "RxFlow is a navigation framework for iOS applications, based on a Reactive Coordinator pattern." From c1d2902308e96a73e051f3e5838458c76201dd4e Mon Sep 17 00:00:00 2001 From: Thibault Wittemberg Date: Sun, 11 Apr 2021 18:02:38 -0400 Subject: [PATCH 4/4] projecT: bump CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc3214f..40b78eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +** Version 2.12.1 **: + +- fix a possible memory leak when the Coordinator's lifecycle was unexpectedly longer than the flow ones (thanks to @asiliuk) + ** Version 2.12.0 **: - bump to RxSwift 6.0.0