Greetings, traveler!
SwiftUI doesn’t provide a native way to track when the user begins the interactive pop gesture—the swipe from the edge to navigate back. While NavigationStack and NavigationView abstract away UIKit, both still rely on UINavigationController behind the scenes. This opens the door to integrating gesture tracking using UIKit components.
This article outlines how to detect when the interactive pop gesture is in progress using UIViewControllerRepresentable and UIGestureRecognizer.
Why It Matters
There are several reasons why an app might want to detect the interactive pop gesture. For example, you might want to pause ongoing tasks, disable user interaction, trigger animations, or cancel network requests while the user is navigating back. Since SwiftUI doesn’t expose this gesture state, accessing UIKit is the only practical solution.
The Core Idea
We embed an invisible UIViewController into the SwiftUI hierarchy. Once embedded, we access its parent UINavigationController and observe the interactivePopGestureRecognizer or interactiveContentPopGestureRecognizer for iOS 26+ target. When the gesture’s state becomes .began or .changed, we report it back into SwiftUI using a closure.
The Gesture Observer
Here’s a minimal and reusable wrapper for tracking the gesture:
struct PopGestureObserver: UIViewControllerRepresentable {
class Coordinator: NSObject {
var onGestureChange: ((Bool) -> Void)?
@objc func handlePopGesture(_ gesture: UIGestureRecognizer) {
let isActive = gesture.state == .began || gesture.state == .changed
onGestureChange?(isActive)
}
}
var onGestureChange: ((Bool) -> Void)?
func makeCoordinator() -> Coordinator {
let coordinator = Coordinator()
coordinator.onGestureChange = onGestureChange
return coordinator
}
func makeUIViewController(context: Context) -> UIViewController {
let controller = UIViewController()
DispatchQueue.main.async {
if let navController = controller.navigationController {
if #available(iOS 26, *) {
navController.interactiveContentPopGestureRecognizer?.addTarget(
context.coordinator,
action: #selector(Coordinator.handlePopGesture(_:))
)
} else {
navController.interactivePopGestureRecognizer?.addTarget(
context.coordinator,
action: #selector(Coordinator.handlePopGesture(_:))
)
}
}
}
return controller
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}This controller is zero-sized and invisible but fully capable of interacting with UIKit’s navigation system.
Using the Observer in a View
You can embed the observer in any screen where you want to track the gesture. Here’s an example:
struct DetailView: View {
@State private var isPopping = false
var body: some View {
VStack(spacing: 20) {
Text("Detail View")
if isPopping {
Text("Pop gesture in progress")
}
}
.background(
PopGestureObserver { isActive in
isPopping = isActive
}
)
.navigationTitle("Detail")
}
}The gesture state is updated in real-time via the isPopping flag.
Example Navigation Context
To see the gesture in action, embed the detail view in a navigation container:
struct ContentView: View {
var body: some View {
NavigationStack {
NavigationLink("Go to Detail", destination: DetailView())
.navigationTitle("Home")
}
}
}Notes on Integration
- The observer relies on the presence of
UINavigationController. If you fully replace system navigation with a custom solution, this method won’t apply. - Be aware that the
interactivePopGestureRecognizeronly becomes active when the back button is visible and the screen allows swipe-to-go-back gestures.
Conclusion
Tracking the interactive pop gesture in SwiftUI requires stepping outside the declarative model and into UIKit. While the integration is simple, it provides crucial access to gesture state that SwiftUI doesn’t yet expose. This technique makes it possible to coordinate navigation gestures with in-progress tasks or UI changes with minimal overhead.
