@@ -292,6 +292,56 @@ final class NavigationStackTests: XCTestCase {
292292 await assertEventuallyEqual ( nav. viewControllers. count, 5 )
293293 await assertEventuallyEqual ( path, [ 1 , 2 , 3 , 4 ] )
294294 }
295+
296+ @MainActor
297+ func testInteractivePopViaGestureAction( ) async throws {
298+ @UIBinding var path = [ Int] ( )
299+ let nav = NavigationStackController ( path: $path) {
300+ UIViewController ( )
301+ }
302+ nav. navigationDestination ( for: Int . self) { number in
303+ ChildViewController ( number: number)
304+ }
305+ try await setUp ( controller: nav)
306+
307+ nav. traitCollection. push ( value: 1 )
308+ await assertEventuallyEqual ( nav. viewControllers. count, 2 )
309+ await assertEventuallyEqual ( path, [ 1 ] )
310+
311+ let interaction = MockInteractiveTransition ( )
312+ let delegate = MockNavigationControllerDelegate ( )
313+ delegate. interactionController = interaction
314+ nav. delegate = delegate
315+
316+ let interactionExpectation = expectation (
317+ description: " navigationController(_:interactionControllerFor:) called "
318+ )
319+ delegate. interactionExpectation = interactionExpectation
320+
321+ await MainActor . run {
322+ _ = nav. popViewController ( animated: true )
323+ }
324+
325+ await fulfillment ( of: [ interactionExpectation] , timeout: 1.0 )
326+
327+ XCTAssertTrue ( delegate. didCallInteractionController)
328+ XCTAssertFalse ( interaction. didFinish)
329+
330+ await MainActor . run {
331+ interaction. update ( 0.5 )
332+ interaction. finish ( )
333+ }
334+
335+ let predicate = NSPredicate ( format: " viewControllers.@count == 1 " )
336+ let vcCountExpectation = XCTNSPredicateExpectation (
337+ predicate: predicate,
338+ object: nav
339+ )
340+ await fulfillment ( of: [ vcCountExpectation] , timeout: 2.0 )
341+
342+ XCTAssertTrue ( interaction. didFinish)
343+ XCTAssertEqual ( nav. viewControllers. count, 1 )
344+ }
295345}
296346
297347private final class ChildViewController : UIViewController {
@@ -317,3 +367,84 @@ private final class ChildViewController: UIViewController {
317367 }
318368 }
319369}
370+
371+ private class MockInteractiveTransition : UIPercentDrivenInteractiveTransition {
372+ private( set) var didFinish = false
373+
374+ override func finish( ) {
375+ super. finish ( )
376+ didFinish = true
377+ }
378+ }
379+
380+ private class MockAnimator : NSObject , UIViewControllerAnimatedTransitioning {
381+ let duration : TimeInterval
382+
383+ init ( duration: TimeInterval = 0.25 ) {
384+ self . duration = duration
385+ super. init ( )
386+ }
387+ func transitionDuration(
388+ using transitionContext: UIViewControllerContextTransitioning ?
389+ ) -> TimeInterval {
390+ return duration
391+ }
392+ func animateTransition(
393+ using transitionContext: UIViewControllerContextTransitioning
394+ ) {
395+ // Basic animation that moves the fromView out and the toView in.
396+ guard
397+ let container = transitionContext. containerView as UIView ? ,
398+ let fromVC = transitionContext. viewController ( forKey: . from) ,
399+ let toVC = transitionContext. viewController ( forKey: . to)
400+ else {
401+ transitionContext. completeTransition ( false )
402+ return
403+ }
404+
405+ let fromView = fromVC. view!
406+ let toView = toVC. view!
407+
408+ // Place toView below and set starting frame
409+ let initialFrame = transitionContext. initialFrame ( for: fromVC)
410+ toView. frame = initialFrame. offsetBy ( dx: initialFrame. width, dy: 0 )
411+ container. addSubview ( toView)
412+
413+ UIView . animate (
414+ withDuration: transitionDuration ( using: transitionContext) ,
415+ delay: 0 ,
416+ options: [ . curveLinear]
417+ ) {
418+ fromView. frame = initialFrame. offsetBy ( dx: - initialFrame. width / 3.0 , dy: 0 )
419+ toView. frame = initialFrame
420+ } completion: { finished in
421+ let cancelled = transitionContext. transitionWasCancelled
422+ transitionContext. completeTransition ( !cancelled)
423+ }
424+ }
425+ }
426+
427+ private class MockNavigationControllerDelegate : NSObject , UINavigationControllerDelegate {
428+ var interactionController : UIPercentDrivenInteractiveTransition ?
429+ var interactionExpectation : XCTestExpectation ?
430+ var didCallInteractionController = false
431+
432+ func navigationController(
433+ _ navigationController: UINavigationController ,
434+ animationControllerFor operation: UINavigationController . Operation ,
435+ from fromVC: UIViewController ,
436+ to toVC: UIViewController
437+ ) -> UIViewControllerAnimatedTransitioning ? {
438+ return MockAnimator ( )
439+ }
440+ func navigationController(
441+ _ navigationController: UINavigationController ,
442+ interactionControllerFor animationController: UIViewControllerAnimatedTransitioning
443+ ) -> UIViewControllerInteractiveTransitioning ? {
444+ didCallInteractionController = true
445+ DispatchQueue . main. async { [ weak self] in
446+ self ? . interactionExpectation? . fulfill ( )
447+ }
448+ return interactionController
449+ }
450+ }
0 commit comments