Swiftui page switch stuck when navigationbar changed in IOS17.2

112 views Asked by At

In Swiftui, when I jump from a page containing Navigationbar to a hidden page in Navigationbar, then swipe back, and then click on the jump button, the page will freeze and not respond.I used Iphone14 promax ,ios17.2.1,Tested normal on iOS 16 Here is my test code

struct TestVew : View {
    var body: some View {
        NavigationView {
            ZStack{
                NavigationLink {
                    SecondView()
                } label: {
                    Text("To SecondView")
                }

            }
            .navigationTitle("First page")
        }
    }
}

struct SecondView : View {
    var body: some View {
        VStack{
            Text("SecondView")
        }
        .navigationBarHidden(true)
    }
}

extension UINavigationController: UIGestureRecognizerDelegate {
    override open func viewDidLoad() {
        super.viewDidLoad()
        interactivePopGestureRecognizer?.delegate = self
    }

    public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        return viewControllers.count > 1
    }
}

I hope that when I click on NavigationLink again, it can jump to the page normally and return with normal gestures. Repeating the entire action should not get stuck

1

There are 1 answers

5
MatBuompy On

The issue seems to be caused by using navigationTitle and navigationBarHidden(true) together. Commenting one of them makes the code run as it should. Why? That's anyone's guess. Now, I have solution for having the swipe gesture work and do any customisation (iOS 15+ compatible) you want to your header:

struct FullSwipeNavigationStack<Content: View>: View {
    
    //MARK: - PROPERTIES
    @ViewBuilder var content: Content
    @State private var customGesture: UIPanGestureRecognizer = {
        let gesture = UIPanGestureRecognizer()
        gesture.name = UUID().uuidString
        gesture.isEnabled = true
        return gesture
    }()
    
    var body: some View {
        NavigationView {
            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
            }
        })
    }
}

/// Custom Environment key for passing Gesture ID to its subviews
fileprivate struct PopNotificationID: EnvironmentKey {
    static var defaultValue: String?
}

extension EnvironmentValues {
    var popGestureID: String? {
        get {
            self[PopNotificationID.self]
        }
        set {
            self[PopNotificationID.self] = newValue
        }
    }
}

extension View {
    
    @ViewBuilder
    func enableFullSwipePop(_ isEnabled: Bool) -> some View {
        self.modifier(FullSwipeModifier(isEnabled: isEnabled))
    }
    
}

fileprivate struct FullSwipeModifier: ViewModifier {
    var isEnabled: Bool
    @Environment(\.popGestureID) private var gestureID
    func body(content: Content) -> some View {
        content
            .onChange(of: isEnabled) { newValue  in
                guard let gestureID else {return}
                NotificationCenter.default.post(
                    name: .init(gestureID), object: nil,
                    userInfo: [
                        "status": newValue
                    ]
                )
            }
            .onDisappear {
                guard let gestureID else {return}
                NotificationCenter.default.post(
                    name: .init(gestureID), object: nil,
                    userInfo: [
                        "status": false
                    ]
                )
            }
    }
}

    struct AttachGestureView: UIViewRepresentable {
    @Binding var gesture: UIPanGestureRecognizer
    
    func makeUIView(context: Context) -> some UIView {
        return UIView()
    }
    
    func updateUIView(_ uiView: UIViewType, context: Context) {
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) {
            // Look for parent controller
            if let parentViewController = uiView.parentViewController {
                if let navigationController = parentViewController.navigationController {
                    if let _ = navigationController.view.gestureRecognizers?.first(where: {
                        $0.name == gesture.name}) {
                        print("Already Attached")
                    } else {
                        navigationController.addFullSwipeGesture(gesture)
                    }
                }
            }
        }
    }
    
    
}

fileprivate extension UINavigationController {
    
    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
    }
}

I know it's a lot of code, but with this you can make any view have the swipe to go back gesture. You use it like this:

struct ContentView: View {
    var body: some View {
        FullSwipeNavigationStack {
            TestVew()
        }
    }
}

struct TestView : View {
    var body: some View {
        
        ZStack{
            NavigationLink("To Second View") {
                SecondView()
            }
            //.navigationTitle("First page") // <-- Replace this with a custom Header
        }
    }
}

Also, as I said, if you want a custo navigation you can check my other answer here Custom Navbar. Let me know if that worked for you!