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 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." 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 = ""; 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