iosswiftuikituigesturerecognizeruiswipegesturerecognizer

UISwipeGestureRecognizer Not Triggering Consistently on a Custom UIView with Blur Effect


I'm building a custom notification system for my iOS app where temporary notification views are displayed on top of the screen. These views have a layered structure with a UIVisualEffectView for a blur effect and a content view containing icons and text.

I want users to dismiss the notification by swiping down. I have successfully found that the swipe gestures are being added to the views, but somehow, the swipes are not being detected, not even taps or pan gestures.

The swipe gesture sometimes fails to trigger consistently. Below is the relevant code:

class NotificationViewController: UIViewController, UIGestureRecognizerDelegate {
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        
        // Show a notification after 1 second
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
            self.showNotification(message: "Swipe down to dismiss!")
        }
    }
    
    func showNotification(message: String) {
        let notificationView = createNotificationView(message: message)
        view.addSubview(notificationView)
        positionNotification(notificationView, above: nil)
        
        // Automatically dismiss after 3 seconds
        DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
            self.dismissNotification(notificationView)
        }
    }
    
    private func createNotificationView(message: String) -> UIView {
        // Create the notification view
        let notificationView = UIView()
        notificationView.backgroundColor = .clear
        notificationView.layer.cornerRadius = 12
        notificationView.layer.borderWidth = 1
        notificationView.layer.borderColor = UIColor.darkGray.cgColor
        notificationView.translatesAutoresizingMaskIntoConstraints = false
        notificationView.isUserInteractionEnabled = true
        
        // Add blur effect
        let blurEffect = UIBlurEffect(style: .systemChromeMaterial)
        let blurEffectView = UIVisualEffectView(effect: blurEffect)
        blurEffectView.frame = notificationView.bounds
        blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        blurEffectView.isUserInteractionEnabled = false
        notificationView.addSubview(blurEffectView)
        
        // Add content to the blur effect view
        let label = UILabel()
        label.text = message
        label.textColor = .label
        label.textAlignment = .center
        label.translatesAutoresizingMaskIntoConstraints = false
        blurEffectView.contentView.addSubview(label)
        NSLayoutConstraint.activate([
            label.centerXAnchor.constraint(equalTo: blurEffectView.contentView.centerXAnchor),
            label.centerYAnchor.constraint(equalTo: blurEffectView.contentView.centerYAnchor)
        ])
        
        // Add swipe gesture recognizer
        let swipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipe(_:)))
        swipeGesture.direction = .down
        swipeGesture.delegate = self
        notificationView.addGestureRecognizer(swipeGesture)  
      
        return notificationView
    }
    
    private func positionNotification(_ notificationView: UIView, above previousNotificationView: UIView?) {
        notificationView.translatesAutoresizingMaskIntoConstraints = false
        if let previousView = previousNotificationView {
            NSLayoutConstraint.activate([
                notificationView.bottomAnchor.constraint(equalTo: previousView.topAnchor, constant: -10),
                notificationView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
                notificationView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.9),
                notificationView.heightAnchor.constraint(equalToConstant: 60)
            ])
        } else {
            NSLayoutConstraint.activate([
                notificationView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 60),
                notificationView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
                notificationView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.9),
                notificationView.heightAnchor.constraint(equalToConstant: 60)
            ])
        }
    }
    
    @objc private func handleSwipe(_ gesture: UISwipeGestureRecognizer) {
        guard let notificationView = gesture.view else { return }
        print("Swipe detected on notification!")
        dismissNotification(notificationView)
    }
    
    private func dismissNotification(_ notificationView: UIView) {
        UIView.animate(withDuration: 0.3, animations: {
            notificationView.alpha = 0
        }) { _ in
            notificationView.removeFromSuperview()
        }
    }

}

Same approach has been working for other views, but they were declared at the parent class level, while this "notification view" is something that is initialized within a function, but the values and everything else works well. This should have worked, so I am not sure what I am missing.

Would love some advice.


Solution

  • Worked around the issue by changing from UIView to UIViewController.

    
    class NotificationViewController: UIViewController {
        private let message: String
        private let textColor: UIColor
        private let backgroundColor: UIColor
        private let iconName: String?
        private let iconColor: UIColor
        private let iconHeight: CGFloat
        private let displayDuration: Double
        
        init(message: String, textColor: UIColor, backgroundColor: UIColor, iconName: String?, iconColor: UIColor, iconHeight: CGFloat, displayDuration: Double) {
            self.message = message
            self.textColor = textColor
            self.backgroundColor = backgroundColor
            self.iconName = iconName
            self.iconColor = iconColor
            self.iconHeight = iconHeight
            self.displayDuration = displayDuration
            super.init(nibName: nil, bundle: nil)
            self.modalPresentationStyle = .overCurrentContext
            self.modalTransitionStyle = .crossDissolve
        }
        
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    
        override func viewDidLoad() {
            super.viewDidLoad()
            self.definesPresentationContext = true
            setupNotificationView()
        }
        
        private func setupNotificationView() {
            self.view.backgroundColor = .clear  // Make sure background is transparent
            
            // Create the notification content view
            let notificationContentView = UIView()
            notificationContentView.backgroundColor = .clear
            notificationContentView.layer.cornerRadius = ViewSetUpManager.mediumCornerRadius
            notificationContentView.layer.borderWidth = 1
            notificationContentView.layer.borderColor = UIColor.darkGray.cgColor
            notificationContentView.clipsToBounds = true
            notificationContentView.isUserInteractionEnabled = true
            
            // Add blur effect
            let blurEffect = UIBlurEffect(style: .systemChromeMaterial)
            let blurEffectView = UIVisualEffectView(effect: blurEffect)
            notificationContentView.addSubview(blurEffectView)
            blurEffectView.snp.makeConstraints { make in
                make.edges.equalToSuperview()
            }
            
            // Add icon and label
            let iconImageView = UIImageView()
            if let iconName = iconName {
                iconImageView.image = UIImage(systemName: iconName)
                iconImageView.contentMode = .scaleAspectFit
                iconImageView.tintColor = iconColor
                iconImageView.isUserInteractionEnabled = false
                blurEffectView.contentView.addSubview(iconImageView)
                iconImageView.snp.makeConstraints { make in
                    make.leading.equalToSuperview().offset(16)
                    make.centerY.equalToSuperview()
                    make.width.height.equalTo(iconHeight)
                }
            }
            
            let label = UILabel()
            label.text = message
            label.font = UIFont.systemFont(ofSize: ViewSetUpManager.shared.genericTitleFontSize(), weight: .regular)
            label.textColor = textColor
            label.numberOfLines = 2
            label.textAlignment = .center
            label.adjustsFontSizeToFitWidth = true
            label.isUserInteractionEnabled = false
            blurEffectView.contentView.addSubview(label)
            label.snp.makeConstraints { make in
                make.leading.equalTo(iconImageView.snp.trailing).offset(8)
                make.trailing.equalToSuperview().inset(16)
                make.centerY.equalToSuperview()
            }
            
            // Add notificationContentView to self.view
            self.view.addSubview(notificationContentView)
            notificationContentView.snp.makeConstraints { make in
                make.bottom.equalTo(self.view.safeAreaLayoutGuide).offset(-(self.tabBarHeight() + 50))
                make.centerX.equalToSuperview()
                make.width.equalToSuperview().multipliedBy(0.9)
                make.height.greaterThanOrEqualTo(60)
            }
            
            // Add swipe gesture recognizer
            let swipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipeGesture(_:)))
            swipeGesture.direction = [.left, .down, .right]
            swipeGesture.delegate = self
            notificationContentView.addGestureRecognizer(swipeGesture)
            
            // Automatically dismiss after displayDuration
            DispatchQueue.main.asyncAfter(deadline: .now() + displayDuration) {
                self.dismiss(animated: true, completion: nil)
            }
        }
        
        private func tabBarHeight() -> CGFloat {
            return self.presentingViewController?.tabBarController?.tabBar.frame.height ?? 0
        }
        
        @objc private func handleSwipeGesture(_ gesture: UISwipeGestureRecognizer) {
            self.dismiss(animated: true, completion: nil)
        }
    }
    
    extension NotificationViewController: UIGestureRecognizerDelegate {
        // Implement delegate methods if necessary
    }