在弹出视图之外的任何地方点击时关闭弹出视图,包括按钮、文本字段等

Close a popup view when tapping anywhere outside it, including buttons, textfields, etc

问题已编辑 因为人们似乎很困惑...

查看下面我的代码并观看所附的“视频”,了解正在发生的事情。弹窗关闭:

这就是我想要的行为。但是,如果用户点击 parent 视图中的按钮或文本字段,弹出窗口不会关闭。因此,在那种情况下弹出窗口仍然弹出。

如何在弹出窗口之外的任何地方检测点击手势,包括按钮、文本字段和任何其他 UI 已经拥有自己的点击处理程序的元素,以便我可以关闭弹出窗口而不劫持行为这些水龙头处理程序?

import UIKit

class ViewController: UIViewController, UIGestureRecognizerDelegate, UITextFieldDelegate {
    
    var popup: UIView!
    var label: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .darkGray
        
        let textfield = UITextField()
        textfield.backgroundColor = .white
        textfield.translatesAutoresizingMaskIntoConstraints = false
        textfield.placeholder = "some text"
        view.addSubview(textfield)
        
        let button1 = UIButton()
        button1.setTitle("Button", for: .normal)
        button1.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
        button1.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(button1)
        
        let button2 = UIButton()
        button2.setTitle("Show Popup", for: .normal)
        button2.addTarget(self, action: #selector(popupButtonTapped), for: .touchUpInside)
        button2.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(button2)
        
        label = UILabel()
        label.textColor = .yellow
        label.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(label)

        NSLayoutConstraint.activate([
            textfield.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            textfield.topAnchor.constraint(equalTo: view.topAnchor, constant: 160),
            textfield.widthAnchor.constraint(equalToConstant: 299),
            textfield.heightAnchor.constraint(equalToConstant: 30),
            button1.topAnchor.constraint(equalTo: textfield.bottomAnchor, constant: 40),
            button1.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            button2.topAnchor.constraint(equalTo: button1.bottomAnchor, constant: 40),
            button2.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            //label.topAnchor.constraint(equalTo: button2.bottomAnchor),
            label.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -260),
            label.centerXAnchor.constraint(equalTo: view.centerXAnchor)
        ])
        
        textfield.delegate = self
        let viewTapGesture = UITapGestureRecognizer(target: self, action: #selector(viewTapped))
        viewTapGesture.delegate = self
        view.addGestureRecognizer(viewTapGesture)
    }
    
    @objc func viewTapped(gestureRecognizer: UITapGestureRecognizer) {
        popup?.isHidden = true
    }

    @objc func buttonTapped(_ button: UIButton) {
        label.text = "Button tapped!"
    }
    
    @objc func popupButtonTapped(_ button: UIButton) {
        if popup == nil {
            popup = UIView()
            popup.backgroundColor = #colorLiteral(red: 1, green: 0.9175537825, blue: 0.79708004, alpha: 1)
            popup.layer.borderWidth = 1
            popup.layer.borderColor = UIColor.black.cgColor
            popup.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(popup)
            
            let stackview = UIStackView()
            stackview.axis = .vertical
            stackview.alignment = .fill
            stackview.distribution = .fillEqually
            stackview.translatesAutoresizingMaskIntoConstraints = false
            popup.addSubview(stackview)
            
            for i in 1...5 {
                let button = UIButton()
                button.setTitle("Selection \(i)", for: .normal)
                button.setTitleColor(.black, for: .normal)
                button.widthAnchor.constraint(equalToConstant: 120).isActive = true
                button.addTarget(self, action: #selector(popupItemTapped), for: .touchUpInside)
                stackview.addArrangedSubview(button)
            }
            
            NSLayoutConstraint.activate([
                stackview.topAnchor.constraint(equalTo: popup.topAnchor),
                stackview.leadingAnchor.constraint(equalTo: popup.leadingAnchor),
                stackview.trailingAnchor.constraint(equalTo: popup.trailingAnchor),
                stackview.bottomAnchor.constraint(equalTo: popup.bottomAnchor),
                popup.topAnchor.constraint(equalTo: button.topAnchor),
                popup.leadingAnchor.constraint(equalTo: button.leadingAnchor),
            ])
        } else {
            popup.isHidden = false
        }
    }
    
    @objc func popupItemTapped(_ button: UIButton) {
        label.text = "\(button.currentTitle!) tapped!"
        popup.isHidden = true
    }
    
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        textField.resignFirstResponder()
        label.text = "You typed:   \(textField.text!)"
        return true
    }

}

这是实际效果。请注意,当您在其中点击进行选择时,弹出窗口会关闭。当您在其外部点击时它也会关闭,但当您在文本字段中点击或点击“按钮”时不会关闭。我希望当您点击弹出窗口外的任何地方时关闭弹出窗口,即使是在文本字段或“按钮”内。如果您通过点击文本字段内部或“按钮”关闭它,他们应该会像往常一样继续响应。

修改后的答案。这使您可以在 pop window 之外点击。我测试过并且有效。

  1. 将此添加到您的 viewDidLoad。

         let tap = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))
         view.addGestureRecognizer(tap)
         let pop = YourPopup()
    
  2. 在执行某些操作后使弹出窗口可见。

  3. 添加以下方法:

    @objc func handleTap(_ sender: UITapGestureRecognizer? = nil) { pop.removeFromSuperview() }

看来我只需要添加以下内容:

    @objc func buttonTapped(_ button: UIButton) {
        popup?.isHidden = true 
        label.text = "Button tapped!"
    }

    func textFieldDidBeginEditing(_ textField: UITextField) {
        popup?.isHidden = true
    }

如果父视图中假设有数百个文本字段和按钮(以及其他 UI 启用了用户交互的元素),我必须对它们中的每一个都执行相同的操作。我希望有一个更通用、更优雅的解决方案。

经过多次摆弄,我终于找到了一种方法,可以随时随地 close/hide 弹出窗口,并且仍然让任何 UI 元素出现在屏幕上的任何位置(包括弹出窗口本身)让水龙头发挥作用。如果点击 弹出窗口内,则选择弹出窗口并关闭弹出窗口。如果点击位于父视图弹出窗口 外部 的任何位置,弹出窗口将关闭。如果点击屏幕上的任何其他 UI 元素,即使屏幕上有一千个元素,弹出窗口也会关闭,但 UI 元素仍会像往常一样响应点击。

我们需要设置 UITapGestureRecognizer,但我们让 gestureRecognizer(_:shouldReceive:) 完成工作而不是 #selector 函数,所以我们设置 action: nil.

// Setup TapGestureRecognizer. The ACTION PARAMETER IS NIL since we do not need a
// selector function. We'll let gestureRecognizer(_:shouldReceive:) do the work.
// But we MUST at least register the gesture recognizer's delegate with the view!
let viewTapGesture = UITapGestureRecognizer(target: self, action: nil)
viewTapGesture.delegate = self
view.addGestureRecognizer(viewTapGesture)

func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
    if !popup.isHidden {
        popup.isHidden = true
    }
    return false
}

这是完整的(修订后的)代码:

import UIKit

class ViewController: UIViewController, UIGestureRecognizerDelegate, UITextFieldDelegate {
    
    var popup: UIView!
    var label: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .darkGray
        
        let textfield = UITextField()
        textfield.backgroundColor = .white
        textfield.translatesAutoresizingMaskIntoConstraints = false
        textfield.placeholder = "some text"
        view.addSubview(textfield)
        
        let button1 = UIButton()
        button1.setTitle("Button", for: .normal)
        button1.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
        button1.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(button1)
        
        let button2 = UIButton()
        button2.setTitle("Show Popup", for: .normal)
        button2.addTarget(self, action: #selector(popupButtonTapped), for: .touchUpInside)
        button2.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(button2)
        
        label = UILabel()
        label.textColor = .yellow
        label.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(label)

        NSLayoutConstraint.activate([
            textfield.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            textfield.topAnchor.constraint(equalTo: view.topAnchor, constant: 160),
            textfield.widthAnchor.constraint(equalToConstant: 299),
            textfield.heightAnchor.constraint(equalToConstant: 30),
            button1.topAnchor.constraint(equalTo: textfield.bottomAnchor, constant: 40),
            button1.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            button2.topAnchor.constraint(equalTo: button1.bottomAnchor, constant: 40),
            button2.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            label.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -260),
            label.centerXAnchor.constraint(equalTo: view.centerXAnchor)
        ])
        
        popup = UIView()
        popup.backgroundColor = #colorLiteral(red: 1, green: 0.9175537825, blue: 0.79708004, alpha: 1)
        popup.layer.borderWidth = 1
        popup.layer.borderColor = UIColor.black.cgColor
        popup.translatesAutoresizingMaskIntoConstraints = false
        popup.isHidden = true
        view.addSubview(popup)

        let stackview = UIStackView()
        stackview.axis = .vertical
        stackview.alignment = .fill
        stackview.distribution = .fillEqually
        stackview.translatesAutoresizingMaskIntoConstraints = false
        popup.addSubview(stackview)

        for i in 1...5 {
            let button = UIButton()
            button.setTitle("Selection \(i)", for: .normal)
            button.setTitleColor(.black, for: .normal)
            button.widthAnchor.constraint(equalToConstant: 120).isActive = true
            button.addTarget(self, action: #selector(popupItemTapped), for: .touchUpInside)
            stackview.addArrangedSubview(button)
        }

        NSLayoutConstraint.activate([
            stackview.topAnchor.constraint(equalTo: popup.topAnchor),
            stackview.leadingAnchor.constraint(equalTo: popup.leadingAnchor),
            stackview.trailingAnchor.constraint(equalTo: popup.trailingAnchor),
            stackview.bottomAnchor.constraint(equalTo: popup.bottomAnchor),
            popup.topAnchor.constraint(equalTo: button2.topAnchor),
            popup.leadingAnchor.constraint(equalTo: button2.leadingAnchor),
        ])
        
        textfield.delegate = self
        
        // Setup TapGestureRecognizer. The ACTION PARAMETER IS NIL since we do not need a
        // selector function. We'll let gestureRecognizer(_:shouldReceive:) do the work.
        // But we MUST at least register the gesture recognizer's delegate with the view!
        let viewTapGesture = UITapGestureRecognizer(target: self, action: nil)
        viewTapGesture.delegate = self
        view.addGestureRecognizer(viewTapGesture)

    }
    
    @objc func buttonTapped(_ button: UIButton) {
        label.text = "Button tapped!"
    }
    
    @objc func popupButtonTapped(_ button: UIButton) {
        popup.isHidden = false
    }
    
    @objc func popupItemTapped(_ button: UIButton) {
        label.text = "\(button.currentTitle!) tapped!"
    }
    
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        label.text = "You typed:   \(textField.text!)"
        return true
    }
    
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
        if !popup.isHidden {
            popup.isHidden = true
        }
        return false
    }
}