This follows on from my prior question.
I have some code that creates a custom navigation stack that enables a left-right swipe to pop a view from the stack. It basically allows the left edge swipe but anywhere on the screen instead of only the left edge.
I want to execute a function when a view is popped off the navigation stack. I do not want this function to run half way through a swipe or before the user lets go. I know I can use onDisappear but this does not run fast enough for my specific case, there is always a delay between when the view actually leaves the view and when the onDisappear is called.
Is there a way to manually call the function as soon as the view is popped off?
import SwiftUI
struct ContentView: View {
@State private var isEnabled: Bool = false
var body: some View {
FullSwipeNavigationStack {
NavigationLink("Leading Swipe View") {
Text("hello").enableFullSwipePop(isEnabled)
}
}
}
}
struct FullSwipeNavigationStack<Content: View>: View {
@ViewBuilder var content: Content
/// Full Swipe Custom Gesture
@State private var customGesture: UIPanGestureRecognizer = {
let gesture = UIPanGestureRecognizer()
gesture.name = UUID().uuidString
gesture.isEnabled = false
return gesture
}()
var body: some View {
NavigationStack {
content
.background {
AttachGestureView(gesture: $customGesture)
}
}
.environment(.popGestureID, customGesture.name)
.onReceive(NotificationCenter.default.publisher(for: .init(customGesture.name ?? "")), perform: { info in
if let userInfo = info.userInfo, let status = userInfo["status"] as? Bool {
customGesture.isEnabled = status
}
})
}
}
extension View {
@ViewBuilder
func enableFullSwipePop(_ isEnabled: Bool) -> some View {
self
.modifier(FullSwipeModifier(isEnabled: isEnabled))
}
}
/// Custom Environment Key for Passing Gesture ID to it's subviews
fileprivate struct PopNotificationID: EnvironmentKey {
static var defaultValue: String?
}
fileprivate extension EnvironmentValues {
var popGestureID: String? {
get {
self[PopNotificationID.self]
}
set {
self[PopNotificationID.self] = newValue
}
}
}
/// Helper View Modifier
fileprivate struct FullSwipeModifier: ViewModifier {
var isEnabled: Bool
/// Gesture ID
@Environment(.popGestureID) private var gestureID
func body(content: Content) -> some View {
content
.onChange(of: isEnabled, initial: true) { oldValue, newValue in
guard let gestureID = gestureID else { return }
NotificationCenter.default.post(name: .init(gestureID), object: nil, userInfo: [
"status": newValue
])
}
.onDisappear(perform: {
guard let gestureID = gestureID else { return }
NotificationCenter.default.post(name: .init(gestureID), object: nil, userInfo: [
"status": false
])
})
}
}
/// Helper Files
fileprivate struct AttachGestureView: UIViewRepresentable {
@Binding var gesture: UIPanGestureRecognizer
func makeUIView(context: Context) -> UIView {
return UIView()
}
func updateUIView(_ uiView: UIView, context: Context) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) {
/// Finding Parent Controller
if let parentViewController = uiView.parentViewController {
if let navigationController = parentViewController.navigationController {
/// Checking if already the gesture has been added to the controller
if let _ = navigationController.view.gestureRecognizers?.first(where: { $0.name == gesture.name }) {
print("Already Attached")
} else {
navigationController.addFullSwipeGesture(gesture)
print("Attached")
}
}
}
}
}
}
fileprivate extension UINavigationController {
/// Adding Custom FullSwipe Gesture
/// Special thanks for this SO Answer
/// https://stackoverflow.com/questions/20714595/extend-default-interactivepopgesturerecognizer-beyond-screen-edge
func addFullSwipeGesture(_ gesture: UIPanGestureRecognizer) {
guard let gestureSelector = interactivePopGestureRecognizer?.value(forKey: "targets") else { return }
gesture.setValue(gestureSelector, forKey: "targets")
view.addGestureRecognizer(gesture)
}
}
fileprivate extension UIView {
var parentViewController: UIViewController? {
sequence(first: self) {
$0.next
}.first(where: { $0 is UIViewController}) as? UIViewController
}
}
2
Answers
From your question and the comments, you need a solution that triggers immediately when the view is popped off the navigation stack, without the delay associated with
.onDisappear
. Using aNavigationStack
with a path could potentially provide a more precise control over the navigation stack, allowing for immediate detection of when a view is popped. background processing, however, would not allow the action to occur precisely when the view is popped.So a combination of monitoring the navigation path for immediate detection of changes, and a configuration of gesture recognizers or direct intervention in the navigation logic, may offer a more suitable solution.
Try and use a
NavigationStack
with aNavigationPath
to monitor changes in the navigation stack, and detect when a view is popped.For the custom gesture recognizer (
UISwipeGestureRecognizer
), make sure it is configured to not interfere with the navigation stack’s default behavior but still allows for detecting a swipe gesture that indicates the user intends to pop the view.In the
DetailView
, you would callsetupCustomGesture
passing the view in which you want to detect the gesture. That could be done in aUIViewRepresentable
if integrating with SwiftUI, making sure the gesture recognizer is attached to the correct view.The idea remains to use:
I have good news. Reading comments an idead popped (no pun intended) to my mind: listening to changes of the path could help us detect whether a view was being popped.
Since it was me who originally posted the code here: Swiftui page switch stuck when navigationbar changed in IOS17.2 of the FullSwipeNavigationStack, It was also me who had to tackle this issue.
I was already working on the solution when I saw the first answer, and of course I had chosen a similar path since it was the most logical road to take. Here’s the code:
So, I tried a simle thing like comparing the count of the oldPath with the new one, this will work assuming your minimum deployment target is iOS 17, but I got a solution for that too: I wrote a customOnChange modifier to handle both iOS 16 and iOS 17+ devices.
Here it is:
If you want to support iOS 16 you’ll have to declare an Int State variable too to keep track of the previous path count. I’ve tested both platforms and it works as expected.
Here’s the result:
Let me know if this can work for you!