Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

When LazyPop is canceled, other actions in the view are disabled. #3

Open
keremcesme opened this issue Dec 24, 2020 · 20 comments
Open

Comments

@keremcesme
Copy link

Hello there is a look like the following. LazyPop is active. The problem is as follows: The user can come back to the previous view by sliding from anywhere on the screen. But if the user gives up and cancels lazypop, the view locks. My "Test 2" button becomes unclickable. How can I solve this?

        Button(action: {
            // Actions:
            self.mode.wrappedValue.dismiss()
        }, label: {
            Text("Test 2")
        })
    }
    .lazyPop()
    .navigationBarHidden(true)
    .navigationBarBackButtonHidden(true)
    
}
@joehinkle11
Copy link
Owner

Very odd, I would have to try it myself. Have you tried changing the order of the modifiers?

@buluoray
Copy link

buluoray commented Apr 1, 2021

I have the same issue

@joehinkle11
Copy link
Owner

Sounds like lazy pop would be too dangerous to use if it locks up UIs. If anyone finds the underlying problem, please comment here or make a PR. If I find free time I'll look into it myself

@bougieL
Copy link

bougieL commented Sep 7, 2021

Same issue

@whereiswhere
Copy link

I think I found out what's the problem is.

In my case, the culprit is the .ignoresSafeArea() modifier, if you add .ignoresSafeArea() modifier after the .lazyPop(), if would make UI locks, after remove the .ignoresSafeArea(), it won't lock anymore.

@joehinkle11
Copy link
Owner

That's interesting. Can anyone else confirm this? We could add a warning in the readme about this

@ssadel
Copy link

ssadel commented Aug 11, 2022

I found that when removing .ignoresSafeArea(), my view is still locked, but only momentarily. I can still scroll in a ScrollView, but I can't tap any buttons

@joehinkle11
Copy link
Owner

Ok thank you. I'll update the readme with a gotcha about .ignoresSafeArea(). @ssadel if you find what else is causing the bug to happen in your code, then I can add another gotcha to the readme.

I'll leave the issue open until a proper workaround is found.

@krispykalsi
Copy link

Found an interesting behaviour.

fromView - the view that is about to be popped but is cancelled by the user

  1. If the fromView contains any text fields, they will not be affected by the cancellation
  2. After tapping any one of them, all the buttons also start working as expected

@gaohomway
Copy link

gaohomway commented Apr 27, 2023

I found that when removing .ignoresSafeArea(), my view is still locked, but only momentarily. I can still scroll in a ScrollView, but I can't tap any buttons

did you solve the problem

@AndrewSB
Copy link

interestingly enough, i've come across this issue while using https://github.com/AndrewSB/SwipeTransition/tree/spm in a UIHostingController, quite similarly to how this library is implemented.

the fact that you're seeing the same issue with buttons become inactive makes me think its a limitation of SwiftUI somehow

@gaohomway
Copy link

@AndrewSB This problem still exists, do you have a solution?

@AndrewSB
Copy link

nope, i'm looking for a way to get around this too

@gaohomway
Copy link

There is another question, how to handle side sliding gestures in Page?

@AndrewSB
Copy link

the solution in SwipeTransition does it, go check out the UIGestureDelegate in it; it tells the gesture recognized to not begin if the view receiving the touch is a UIPage

@gaohomway
Copy link

Can you give the key code, thank you very much

If the Page gesture is detected, why not keep the original side swipe gesture, why choose not to work? The user cannot close the page

@ejbills
Copy link

ejbills commented Jul 10, 2023

is there any way to fix this ATM?

@lukewusb
Copy link

lukewusb commented Nov 5, 2024

Hi there, just FYI, we have the same issue, which was resolved after we tried the solution from StackOverflow. We have no idea why it works, but it indeed fixes the problem.

https://stackoverflow.com/questions/78554872/swiftui-tap-gestures-not-responding-in-swiftui-view-after-uikit-interactive-tran

@cjhodge
Copy link

cjhodge commented Dec 31, 2024

Hi there, just FYI, we have the same issue, which was resolved after we tried the solution from StackOverflow. We have no idea why it works, but it indeed fixes the problem.

https://stackoverflow.com/questions/78554872/swiftui-tap-gestures-not-responding-in-swiftui-view-after-uikit-interactive-tran

what was your solution?

@RaziPour1993
Copy link

RaziPour1993 commented Jan 4, 2025

Fixed SwiftUI touch event handling after LazyPop gesture cancellation
Just changed swipe left to pop for RTL


import SwiftUI
import UIKit

/// A custom structure that enables interactive pop navigation gestures in SwiftUI views.
/// This component allows for a native-feeling swipe-to-go-back gesture similar to UIKit's default behavior.
fileprivate struct LazyPop<Content: View>: UIViewControllerRepresentable {
    let rootView: Content
    @Binding var isEnabled: Bool
    
    /// Initializes a LazyPop structure with customizable pop behavior.
    /// - Parameters:
    ///   - rootView: The SwiftUI view to be displayed and enhanced with pop gesture.
    ///   - isEnabled: A binding to dynamically enable or disable the pop gesture functionality.
    ///                If nil, the gesture remains permanently enabled.
    ///   - onCancel: An optional closure that gets called when the pop gesture is cancelled.
    ///               This can be used to handle cleanup or state reset when pop is abandoned.
    init(_ rootView: Content, isEnabled: (Binding<Bool>)? = nil, onCancel: (() -> Void)? = nil) {
        self.rootView = rootView
        self._isEnabled = isEnabled ?? Binding<Bool>(get: { true }, set: { _ in })
    }
    
    /// Creates a new view controller to manage the SwiftUI view.
    func makeUIViewController(context: Context) -> UIViewController {
        let vc = SwipeLeftToPopViewController(rootView: rootView)
        vc.lazyPopContent = self
        return vc
    }
    
    /// Updates the view controller with new data.
    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
        guard let host = uiViewController as? UIHostingController<Content> else { return }
        host.rootView = rootView
    }
}

extension View {
    /// Adds an interactive pop gesture navigation to a SwiftUI view.
    /// 
    /// Example usage:
    /// ```swift
    /// ContentView()
    ///     .lazyPop(isEnabled: $isPopEnabled) {
    ///         print("Pop was cancelled")
    ///     }
    /// ```
    ///
    /// - Parameters:
    ///   - isEnabled: Optional binding to control when the pop gesture is active.
    ///                When nil or true, the gesture is enabled.
    ///   - onCancel: Optional closure that executes when the pop gesture is cancelled.
    /// - Returns: A view with interactive pop gesture capability.
    public func lazyPop(isEnabled: (Binding<Bool>)? = nil, onCancel: (() -> Void)? = nil) -> some View {
        LazyPop(self, isEnabled: isEnabled, onCancel: onCancel)
    }
}

/// A custom animation controller that provides a smooth sliding transition effect
/// when navigating between view controllers.
class SlideAnimatedTransitioning: NSObject, UIViewControllerAnimatedTransitioning {
    
    /// Performs the animation for the transition.
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        guard let fromView = transitionContext.viewController(forKey: .from)?.view,
              let toView = transitionContext.viewController(forKey: .to)?.view else { return }
        
        let containerView = transitionContext.containerView
        let width = containerView.frame.width
        
        // Set initial positions using layer transforms instead of frames
        toView.frame = containerView.bounds
        toView.layer.transform = CATransform3DMakeTranslation(width / 3.33, 0, 0)
        fromView.layer.shadowRadius = 5.0
        fromView.layer.shadowOpacity = 1.0
        toView.layer.opacity = 0.9
        
        containerView.insertSubview(toView, belowSubview: fromView)
        
        UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, options: .curveLinear, animations: {
            toView.layer.transform = CATransform3DIdentity
            fromView.layer.transform = CATransform3DMakeTranslation(-width + 1, 0, 0)
            toView.layer.opacity = 1.0
            fromView.layer.shadowOpacity = 0.1
        }, completion: { _ in
            // Reset transforms on completion
            toView.layer.transform = CATransform3DIdentity
            fromView.layer.transform = CATransform3DIdentity
            toView.layer.opacity = 1.0
            fromView.layer.opacity = 1.0
            transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
        })
    }
    
    /// Returns the duration of the transition animation.
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        0.3
    }
}

/// A specialized view controller that implements custom swipe-to-pop navigation.
/// This controller manages the interactive gesture recognition and transition animations
/// for pop operations.
class SwipeLeftToPopViewController<Content>: UIHostingController<Content>, UINavigationControllerDelegate where Content: View {
    
    fileprivate var lazyPopContent: LazyPop<Content>?
    private var percentDrivenInteractiveTransition: UIPercentDrivenInteractiveTransition?
    private var panGestureRecognizer: UIPanGestureRecognizer!
    private var parentNavigationControllerToUse: UINavigationController?
    private var gestureAdded = false
    
    /// Adds the gesture recognizer when the view's layout is updated.
    override func viewDidLayoutSubviews() {
        addGesture()
    }
    
    /// Adds a pan gesture recognizer to the view.
    public func addGesture() {
        guard !gestureAdded else { return }
        
        var currentVc: UIViewController = self
        while let parent = currentVc.parent {
            if let navController = currentVc.navigationController, navController.viewControllers.count > 1 {
                parentNavigationControllerToUse = navController
                break
            }
            currentVc = parent
        }
        
        guard parentNavigationControllerToUse?.viewControllers.count ?? 0 > 1 else { return }
        
        panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
        view.addGestureRecognizer(panGestureRecognizer)
        gestureAdded = true
    }
    
    /// Handles the pan gesture to perform the pop action.
    @objc func handlePanGesture(_ panGesture: UIPanGestureRecognizer) {
        let total = parentNavigationControllerToUse?.view.frame.width ?? view.frame.width
        let percent = max(-panGesture.translation(in: view).x, 0) / total
        
        switch panGesture.state {
        case .began:
            if lazyPopContent?.isEnabled == true {
                parentNavigationControllerToUse?.delegate = self
                _ = parentNavigationControllerToUse?.popViewController(animated: true)
            }
        case .changed:
            percentDrivenInteractiveTransition?.update(percent)
        case .ended:
            let velocity = panGesture.velocity(in: view).x
            if percent > 0.5 || velocity < -100 {
                percentDrivenInteractiveTransition?.finish()
            } else {
                percentDrivenInteractiveTransition?.cancel()
            }
        case .cancelled, .failed:
            percentDrivenInteractiveTransition?.cancel()
        default:
            break
        }
    }
    
    /// Provides the animation controller for the navigation transition.
    func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        SlideAnimatedTransitioning()
    }
    
    /// Provides the interaction controller for the navigation transition.
    func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
        parentNavigationControllerToUse?.delegate = nil
        navigationController.delegate = nil
        
        if panGestureRecognizer.state == .began {
            percentDrivenInteractiveTransition = UIPercentDrivenInteractiveTransition()
            percentDrivenInteractiveTransition?.completionCurve = .easeOut
        } else {
            percentDrivenInteractiveTransition = nil
        }
        
        return percentDrivenInteractiveTransition
    }
}

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests