Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public final class BatchRemovingTransitionContext: TransitionContext {

private let onCompleted: (BatchRemovingTransitionContext) -> Void
private var childContexts: [ChildContext] = []
private var hasNotifiedCompletion: Bool = false
private var callbacks: [(CompletionEvent) -> Void] = []

init(
Expand All @@ -56,6 +57,7 @@ public final class BatchRemovingTransitionContext: TransitionContext {

public func notifyCompleted() {
assert(Thread.isMainThread)
guard isInvalidated == false, isCompleted == false else { return }
isCompleted = true
onCompleted(self)
}
Expand Down Expand Up @@ -91,6 +93,8 @@ public final class BatchRemovingTransitionContext: TransitionContext {
override func invalidate() {
assert(Thread.isMainThread)
isInvalidated = true
guard hasNotifiedCompletion == false else { return }
hasNotifiedCompletion = true
callbacks.forEach { $0(.interrupted) }
}

Expand All @@ -106,6 +110,8 @@ public final class BatchRemovingTransitionContext: TransitionContext {
Triggers ``addCompletionEventHandler(_:)`` with ``TransitionContext/CompletionEvent/succeeded``
*/
func transitionSucceeded() {
callbacks.forEach{ $0(.succeeded) }
guard hasNotifiedCompletion == false else { return }
hasNotifiedCompletion = true
callbacks.forEach { $0(.succeeded) }
}
}
6 changes: 6 additions & 0 deletions Sources/FluidStack/Transition/RemovingTransitionContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public final class RemovingTransitionContext: TransitionContext {
private let onRequestedDisplayOnTop:
(DisplaySource) -> FluidStackController.DisplayingOnTopSubscription

private var hasNotifiedCompletion: Bool = false
private var callbacks: [(CompletionEvent) -> Void] = []

init(
Expand Down Expand Up @@ -57,6 +58,7 @@ public final class RemovingTransitionContext: TransitionContext {
assert(Thread.isMainThread)
guard isInvalidated == false, isCompleted == false else { return }
isInvalidated = true
hasNotifiedCompletion = true
callbacks.forEach { $0(.cancelled) }
onAnimationCompleted(self)
}
Expand Down Expand Up @@ -97,6 +99,8 @@ public final class RemovingTransitionContext: TransitionContext {
override func invalidate() {
assert(Thread.isMainThread)
isInvalidated = true
guard hasNotifiedCompletion == false else { return }
hasNotifiedCompletion = true
callbacks.forEach { $0(.interrupted) }
}

Expand All @@ -115,6 +119,8 @@ public final class RemovingTransitionContext: TransitionContext {
Triggers ``addCompletionEventHandler(_:)`` with ``TransitionContext/CompletionEvent/succeeded``
*/
func transitionSucceeded() {
guard hasNotifiedCompletion == false else { return }
hasNotifiedCompletion = true
callbacks.forEach { $0(.succeeded) }
}

Expand Down
12 changes: 8 additions & 4 deletions Sources/FluidStack/ViewController/FluidStackController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -718,13 +718,17 @@ open class FluidStackController: UIViewController {
fromViewControllers: itemsToRemove.map(\.viewController),
toViewController: targetTopItem?.viewController,
onCompleted: { [weak self] context in

assert(Thread.isMainThread)

guard let self = self else {
return
return
}


guard context.isInvalidated == false else {
return
}

/**
Completion of transition, cleaning up
*/
Expand Down
62 changes: 62 additions & 0 deletions Tests/FluidStackTests/FluidStackControllerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,68 @@ final class FluidStackControllerTests: XCTestCase {

}

/// Tests that batch removing completion is called exactly once.
/// When fluidPop triggers batch removing (VC is not on top),
/// the completion must fire exactly once after the batch transition completes.
func testBatchRemovingCompletionCalledOnce() {

let window = UIWindow()
let stack = FluidStackController()
window.rootViewController = stack
window.makeKeyAndVisible()

let vc1 = UIViewController()
let vc2 = UIViewController()
let vc3 = UIViewController()
let vc4 = UIViewController()

stack.addContentViewController(vc1, transition: .disabled)
stack.addContentViewController(vc2, transition: .disabled)
stack.addContentViewController(vc3, transition: .disabled)
stack.addContentViewController(vc4, transition: .disabled)

XCTAssertEqual(stack.stackingViewControllers.count, 4)

let exp = expectation(description: "completion called")
exp.assertForOverFulfill = true
var completionCallCount = 0

vc2.fluidPop { _ in
completionCallCount += 1
exp.fulfill()
}

wait(for: [exp], timeout: 2)

XCTAssertEqual(stack.stackingViewControllers.count, 1)
XCTAssertEqual(completionCallCount, 1, "Completion should be called exactly once")
}

/// Tests that removeAllViewController completion is called exactly once.
func testRemoveAllViewControllerCompletionCalledOnce() {

let stack = FluidStackController()

stack.addContentViewController(UIViewController(), transition: .disabled)
stack.addContentViewController(UIViewController(), transition: .disabled)
stack.addContentViewController(UIViewController(), transition: .disabled)
stack.addContentViewController(UIViewController(), transition: .disabled)

XCTAssertEqual(stack.stackingViewControllers.count, 4)

var completionCallCount = 0

stack.removeAllViewController(
transition: .disabled,
completion: { event in
completionCallCount += 1
}
)

XCTAssertEqual(stack.stackingViewControllers.count, 1)
XCTAssertEqual(completionCallCount, 1, "Completion should be called exactly once")
}

final class ContentTypeOption: UIViewController {
init(contentType: FluidStackContentConfiguration.ContentType) {
super.init(nibName: nil, bundle: nil)
Expand Down
Loading