UIGestureRecognizer works outside of UIView borders instead of inside

I've implemented a popup notification in my app to show user the data was being updated successfully or not.

This popup is just a UIView instantiated from .xib. Here is screenshot of elements hierarchy in xib file: https://cln.sh/te8JhM

Also I wanted the ability to swipe out the popup if user don't want to see a full time it shows itself on the screen. To do that I have implemented a UIGestureRecognizer:

private func configureTapGesture() {
    guard let window = UIApplication.shared.windows.first(where: {$0.isKeyWindow}) else { return }
    let swipe = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipes(_:)))
    swipe.direction = .up
    window.addGestureRecognizer(swipe)
}

@objc private func handleSwipes(_ sender:UISwipeGestureRecognizer) {
    if sender.direction == .up {
        UIView.animate(withDuration: 0.15, delay: 0.0, options: .curveLinear) {
            self.popupView.center.y -= 50
        } completion: { _ in
            self.popupView.removeFromSuperview()
        }
        self.isRemovedByTap = true
    }
}

When I run the app it works only if I swipe out anywhere but not inside the popupView. Please check for the GIF: https://cln.sh/sNm5RN

If I replace a target from self (Popup class) to popupView (UIVisualEffectView, which is a rounded rectangle you see on GIF), then I receive an error unrecognized selector sent to instance

Here is a my full custom Popup class where I initialize the view, configure, animate and show it:

import UIKit

  class PopupView: UIView {

@IBOutlet weak var popupView: UIVisualEffectView!
@IBOutlet weak var symbol: UIImageView!
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var descriptionLabel: UILabel!

private var isRemovedByTap = false

override init(frame: CGRect) {
    super.init(frame: frame)
    configure()
}

required init?(coder: NSCoder) {
    super.init(coder: coder)
    configure()
}

private func configure() {
    if let views = Bundle.main.loadNibNamed("PopupView", owner: self) {
        guard let view = views.first as? UIView else { return }
        view.frame = bounds
        addSubview(view)
    }
}

private func configurePopup() {
    guard let window = UIApplication.shared.windows.first(where: {$0.isKeyWindow}) else { return }
    popupView.layer.cornerRadius = 20
    popupView.clipsToBounds = true
    popupView.center.x = window.frame.midX
    popupView.translatesAutoresizingMaskIntoConstraints = true
    window.addSubview(popupView)
}

private func animatePopup() {
    UIView.animate(withDuration: 0.15, delay: 0.0, options: .curveLinear) {
        self.popupView.center.y += 34
    } completion: { _ in
        UIView.animate(withDuration: 0.15, delay: 10.0, options: .curveLinear) {
            self.popupView.center.y -= 50
        } completion: { _ in
            if !self.isRemovedByTap {
                self.popupView.removeFromSuperview()
            }
        }
    }
}

private func configureTapGesture() {
    guard let window = UIApplication.shared.windows.first(where: {$0.isKeyWindow}) else { return }
    let swipe = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipes(_:)))
    swipe.direction = .up
    window.addGestureRecognizer(swipe)
}

@objc private func handleSwipes(_ sender:UISwipeGestureRecognizer) {
    if sender.direction == .up {
        UIView.animate(withDuration: 0.15, delay: 0.0, options: .curveLinear) {
            self.popupView.center.y -= 50
        } completion: { _ in
            self.popupView.removeFromSuperview()
        }
        self.isRemovedByTap = true
    }
}

func showPopup(title: String, message: String, symbol: UIImage) {
    titleLabel.text = title
    descriptionLabel.text = message
    self.symbol.image = symbol
    
    configurePopup()
    animatePopup()
    configureTapGesture()
}
}

What should I change to be able to swipe out only when inside the popupView rounded rectangle?

Answered by artexhibit in 709061022

Here is how I was able to achieve my needs:

  1. Switch popupView.isUserInteractionEnabled = false to start swipe recognises not only outside the view

  2. change my didSwipe method onto this:

 @objc private func didSwipe(_ sender:UISwipeGestureRecognizer) {
        guard let window = UIApplication.shared.windows.first(where: {$0.isKeyWindow}) else { return }
        let tappedArea = sender.location(in: popupView)
        let popupFrame = CGRect(x: tappedArea.x, y: tappedArea.y, width: popupView.frame.width, height: popupView.frame.height)
        
        if sender.direction == .up, window.frame.contains(popupFrame) {
            UIView.animate(withDuration: 0.15, delay: 0.0, options: .curveLinear) {
                self.popupView.center.y -= 50
            } completion: { _ in
                self.popupView.removeFromSuperview()
            }
            self.isRemovedBySwipe = true
        }
    }

Now I am able to swipe out the popup only when swipe inside its borders and ignore swipes outside of it.

Accepted Answer

Here is how I was able to achieve my needs:

  1. Switch popupView.isUserInteractionEnabled = false to start swipe recognises not only outside the view

  2. change my didSwipe method onto this:

 @objc private func didSwipe(_ sender:UISwipeGestureRecognizer) {
        guard let window = UIApplication.shared.windows.first(where: {$0.isKeyWindow}) else { return }
        let tappedArea = sender.location(in: popupView)
        let popupFrame = CGRect(x: tappedArea.x, y: tappedArea.y, width: popupView.frame.width, height: popupView.frame.height)
        
        if sender.direction == .up, window.frame.contains(popupFrame) {
            UIView.animate(withDuration: 0.15, delay: 0.0, options: .curveLinear) {
                self.popupView.center.y -= 50
            } completion: { _ in
                self.popupView.removeFromSuperview()
            }
            self.isRemovedBySwipe = true
        }
    }

Now I am able to swipe out the popup only when swipe inside its borders and ignore swipes outside of it.

UIGestureRecognizer works outside of UIView borders instead of inside
 
 
Q