Search code examples
iosswiftuiviewuikituitapgesturerecognizer

Have User be able to Swipe Down on temporary UIView


I wanted to be able to have the user swipe down to dismiss a temporary Notification that comes in from the bottom.

Here's what the code looks like:

func showAnimationToast(...) {
    
        let toastView = UIView(frame: CGRect(x: 10, y: view.frame.size.height - view.safeAreaInsets.bottom, width: view.frame.size.width - 20, height: 60))
        ...
        toastView.tag = 1474
        
        let animationView = AnimationView(name: animationName)
        ...
        toastView.addSubview(animationView)
        
        let messageLabel = UILabel(frame: CGRect(x: toastView.frame.size.height, y: 5, width: toastView.frame.size.width - toastView.frame.size.height, height: 50))
        ...
        toastView.addSubview(messageLabel) 
        toastView.isUserInteractionAvailable = true

I tried to add a UISwipeGestureRecognizer to toastView, but it never worked. I even tried the simple UITapGestureRecognizer and it STILL didn't work.

Here's what I tried:


        //Let Swipe Down Dismiss. Does not work
        let swipeDownGesture = UISwipeGestureRecognizer(target: self, action: #selector(dismissToast(_:)))
        swipeDownGesture.direction = .down
        toastView.addGestureRecognizer(swipeDownGesture)
        
        //Show animation
        UIView.animate(withDuration: 0.2, delay: 0, animations: {
            toastView.frame.origin.y = self.view.frame.size.height - self.view.safeAreaInsets.bottom - 70
        }, completion: {_ in
            animationView.play()
        })
        
        //Remove after specified time
        UIView.animate(withDuration: 0.2, delay: duration, animations: {
            toastView.center.y = self.view.frame.size.height + 50
        }, completion: {_ in
            toastView.removeFromSuperview()
        })
    }
    
    @objc func dismissToast(_ sender: UIGestureRecognizer) {
        print("dismiss")
        let toastView = view.subviews.filter { view in
            if view.tag == 1474 /*toastView*/ tag{
                return true
            }
            return false
        }.last!
        
        UIView.animate(withDuration: 0.2, delay: 0, animations: {
            toastView.center.y = self.view.frame.size.height + 50
        }, completion: {_ in
            toastView.removeFromSuperview()
        })
        
    }

Solution

  • The issue seems to be that while a view is waiting for an animation to play (during the "delay" period), it can't receive user interactions.

    One way to work around this is to not use the delay parameter, and instead use DispatchQueue.main.asyncAfter:

        UIView.animate(withDuration: 0.2, delay: 0, options: [.allowUserInteraction], animations: {
            toastView.frame.origin.y = self.view.frame.size.height - self.view.safeAreaInsets.bottom - 70
        }, completion: {_ in
            DispatchQueue.main.asyncAfter(deadline: .now() + duration + 0.2) {
                UIView.animate(withDuration: 0.2, delay: 0, animations: {
                    toastView.center.y = self.view.frame.size.height + 50
                }, completion: {_ in
                    toastView.removeFromSuperview()
                })
            }
    
        })