Skip to content

Commit cc167df

Browse files
FIX: NavigationStackController subclass delegate methods are not called (#307)
* Add interactive navigation tests to NavigationStackTests suite * Fix: delegate's methods of the subclass of NavigationStackController are not called * Clean up tests --------- Co-authored-by: takehilo <[email protected]>
1 parent 850e569 commit cc167df

File tree

2 files changed

+147
-1
lines changed

2 files changed

+147
-1
lines changed

Examples/CaseStudiesTests/NavigationStackTests.swift

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

297347
private final class ChildViewController: UIViewController {
@@ -317,3 +367,85 @@ 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+
428+
private class MockNavigationControllerDelegate: NSObject, UINavigationControllerDelegate {
429+
var interactionController: UIPercentDrivenInteractiveTransition?
430+
var interactionExpectation: XCTestExpectation?
431+
var didCallInteractionController = false
432+
433+
func navigationController(
434+
_ navigationController: UINavigationController,
435+
animationControllerFor operation: UINavigationController.Operation,
436+
from fromVC: UIViewController,
437+
to toVC: UIViewController
438+
) -> UIViewControllerAnimatedTransitioning? {
439+
return MockAnimator()
440+
}
441+
func navigationController(
442+
_ navigationController: UINavigationController,
443+
interactionControllerFor animationController: UIViewControllerAnimatedTransitioning
444+
) -> UIViewControllerInteractiveTransitioning? {
445+
didCallInteractionController = true
446+
DispatchQueue.main.async { [weak self] in
447+
self?.interactionExpectation?.fulfill()
448+
}
449+
return interactionController
450+
}
451+
}

Sources/UIKitNavigation/Navigation/NavigationStackController.swift

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,9 +196,23 @@
196196
weak var base: (any UINavigationControllerDelegate)?
197197

198198
override func responds(to aSelector: Selector!) -> Bool {
199-
aSelector == #selector(navigationController(_:didShow:animated:))
199+
#if !os(tvOS) && !os(watchOS)
200+
aSelector == #selector(navigationController(_:willShow:animated:))
201+
|| aSelector == #selector(navigationController(_:didShow:animated:))
202+
|| aSelector == #selector(navigationControllerSupportedInterfaceOrientations(_:))
203+
|| aSelector == #selector(navigationControllerPreferredInterfaceOrientationForPresentation(_:))
204+
|| aSelector == #selector(navigationController(_:interactionControllerFor:))
205+
|| aSelector == #selector(navigationController(_:animationControllerFor:from:to:))
200206
|| MainActor._assumeIsolated { base?.responds(to: aSelector) }
201207
?? false
208+
#else
209+
aSelector == #selector(navigationController(_:willShow:animated:))
210+
|| aSelector == #selector(navigationController(_:didShow:animated:))
211+
|| aSelector == #selector(navigationController(_:interactionControllerFor:))
212+
|| aSelector == #selector(navigationController(_:animationControllerFor:from:to:))
213+
|| MainActor._assumeIsolated { base?.responds(to: aSelector) }
214+
?? false
215+
#endif
202216
}
203217

204218
func navigationController(

0 commit comments

Comments
 (0)