Question EDITED since it seems people were confused...
See my code below and watch the attached "video" of what is happening. The popup closes:
This is the behavior I want. BUT the popup does NOT close if the user taps on a button or textfield in the parent view. Consequently, the popup remains popped up in that situation.
How do I detect a tap gesture anywhere OUTSIDE the popup, including buttons, textfields, and any other UI elements that already have their own tap handlers, so that I can close the popup without hijacking the behaviors of those tap handlers?
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
}
}
Here's what it looks like in action. Note that the popup closes when you tap inside it to make a selection. It also closes when you tap outside it, but NOT when you tap in the textfield or tap on "Button". I want the popup to close when you tap anywhere outside it, even inside the textfield or "Button". And if you close it by tapping inside the textfield or on "Button", they should continue to respond as usual.
After much fiddling around, I finally found a way to close/hide the popup any time and anywhere the tap occurs and still let whatever UI element anywhere on the screen (including the popup itself) that got the tap do its thing. If the tap is inside the popup, the popup selection is made and the popup closes. If the tap is anywhere outside the popup on the parent view, the popup closes. If the tap is made on any other UI element on the screen, even if there were a thousand of them on the screen, the popup also closes but the UI element still responds to the tap as usual.
We need to set up the UITapGestureRecognizer, but instead of having a #selector function, we let gestureRecognizer(_:shouldReceive:)
do the work, so we set 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)
and
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
if !popup.isHidden {
popup.isHidden = true
}
return false
}
Here's the full (revised) code:
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
}
}