Search code examples
swiftuiviewuitextfielduitextviewuitapgesturerecognizer

Swift iOS -Remove View When TextField, TextView, and Background Tapped But Losing Events


enter image description here enter image description here

First let me say I play around with programmatic but I'm currently a novice at it.

I have mix of programmatic views and storyboard objects:

StoryBoard Objects:

  1. Button
  2. TextField
  3. TextView

Programmatic Views:

  1. messageLabel
  2. viewForMessageLabel

When I press the button the viewForMessageLabel is added. In viewDidLoad I add a tap gesture to remove the viewForMessageLabel when the background is tapped. I also add the same tap gesture to the textField to remove the viewForMessageLabel if it's present. I again add the same tap gesture to the textField to remove it also.

If the keyboard is present I add another tap gesture to in viewDidLoad to the textField to dismiss it also. I notice things are wacky and I lose touch events.

If I press the button to add the label when I touch the background it doesn't get dismissed. If I press the textField it will dismiss it and show the keyboard. While the textField is still up if I press the button again, the label appears, I press the textField again and nothing happens. When I press return to hide the keyboard (I implemented the method), the keyboard disappears, press the button, the viewForMessageLabel appears, and I now when I press the textField the viewForMessageLabel disappears. Basically the same thing is happening with the textField.

What I want is

  1. If the viewForMessageLabel is present and I press either the background, textField, or textView it should disappear.

  2. If the textField's or textView's keyboard is present and I press the background the keyboard should disappear also.

My code:

class ViewController: UIViewController, UITextFieldDelegate, UITextViewDelegate {

    //MARK:- Outlets
    @IBOutlet weak var textField: UITextField!
    @IBOutlet weak var textView: UITextView!
    @IBOutlet weak var button: UIButton!

    let messagelabel: UILabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.text = "Pizza Pizza Pizza Pizza Pizza"
        label.font = UIFont(name: "Helvetica-Regular", size: 17)
        label.sizeToFit()
        label.numberOfLines = 0
        label.textAlignment = .center
        label.textColor = UIColor.white
        label.backgroundColor = UIColor.clear
        return label
    }()

    let viewForMessageLabel: UIView = {
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.backgroundColor = UIColor.red
        return view
    }()

    //View Controller Lifecycle
    override func viewDidLoad() {
        super.viewDidLoad()

        textField.delegate = self
        textView.delegate = self

        // 0. hide viewForMessageLabel is background is tapped
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(removeViewForMessageLabel))
        view.addGestureRecognizer(tapGesture)

        // 1. hide viewForMessageLabel if textView is tapped
        textView.addGestureRecognizer(tapGesture)

        // 2. hide keyboard if background if tapped
        let hideKeyboard = UITapGestureRecognizer(target: self, action: #selector(hideKeyboardWhenBackGroundTapped))
        view.addGestureRecognizer(hideKeyboard)

        // 3. hide keyboard if textView is tapped
        textView.addGestureRecognizer(hideKeyboard)

        // 4. hide viewForMessageLabel for textField if background is tapped
        textField.addTarget(self, action: #selector(removeViewForMessageLabel), for: .editingDidBegin)

    }

    //MARK:- Button
    @IBAction func buttonPressed(_ sender: UIButton) {
        view.addSubview(viewForMessageLabel)
        setViewForMessageLabelAnchors()
        setMessageLabelAnchors()
    }

    //MARK:- Functions
    func setViewForMessageLabelAnchors(){
        viewForMessageLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 44).isActive = true
        viewForMessageLabel.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0).isActive = true
        viewForMessageLabel.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0).isActive = true
        viewForMessageLabel.addSubview(messagelabel)
    }

    func setMessageLabelAnchors(){
        messagelabel.topAnchor.constraint(equalTo: viewForMessageLabel.topAnchor, constant: 0).isActive = true
        messagelabel.widthAnchor.constraint(equalTo: viewForMessageLabel.widthAnchor).isActive = true
        viewForMessageLabel.bottomAnchor.constraint(equalTo: messagelabel.bottomAnchor, constant: 0).isActive = true
    }

    func removeViewForMessageLabel(){
        viewForMessageLabel.removeFromSuperview()
    }

    func hideKeyboardWhenBackGroundTapped(){
        textField.resignFirstResponder()
    }

    //MARK:- TextField Delegate
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        view.endEditing(true)
        return true
    }

    func textViewDidBeginEditing(_ textView: UITextView) {
        removeViewForMessageLabel()
    }

    //MARK:- TextView Delegate
    func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
        if(text == "\n") {
            textView.resignFirstResponder()
            return false
        }
        return true
    }
}

Solution

  • This doesn't exactly answer the question but I found a work around. If I use the method @Toddg suggested:

    func removeLabelAndHideKeyboard() {
        viewForMessageLabel.removeFromSuperview()
        textField.resignFirstResponder()
    }
    

    It adds resigning the textField to the function which helped tremendously.

    Also inside viewDidLoad I added:

    textField.addTarget(self, action: #selector(removeViewForMessageLabel), for: .touchDown)
    

    The key there is to use .touchDown and NOT .editingDidBegin. This way I can go back and forth in between the textField and the textView and the keyboard will respond to both. I had to add 1 more thing -a toolBar to the textView's keyboard which has Done button on it to dismiss the textView:

        func addDoneButtonOnKeyboard(){
            let toolBar = UIToolbar()
            toolBar.sizeToFit()
            doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(dismissKeyboard))
            toolBar.setItems([doneButton!], animated: true)
            textView.inputAccessoryView = toolBar
        }
    
        @objc func dismissTextViewKeyboard(){
            view.endEditing(true)
        }
    

    This way when the textView is present I can dismiss it.

    In all situations if I press the textField, background, or textView and the viewForMessageLabel is present it will disappear.

    If the textField is first responder and it's keyboard is present and I press the background it will disappear.

    I have not figured out how to also dismiss the textView when the background is touched in addition to everything else so I implemented a Done button on the toolBar instead. If I press it and the textView's keyboard is present will get dismissed when it calls the dismissTextViewKeyboard() function I added in. Both are at the bottom and everything else is in viewDidLoad.

    If anyone has a better answer I'll up vote it.

    class ViewController: UIViewController, UITextFieldDelegate, UITextViewDelegate {
    
    
        //MARK:- Outlets
        @IBOutlet weak var textField: UITextField!
        @IBOutlet weak var textView: UITextView!
        @IBOutlet weak var button: UIButton!
    
        let messagelabel: UILabel = {
            let label = UILabel()
            label.translatesAutoresizingMaskIntoConstraints = false
            label.text = "Pizza Pizza Pizza Pizza Pizza"
            label.font = UIFont(name: "Helvetica-Regular", size: 17)
            label.sizeToFit()
            label.numberOfLines = 0
            label.textAlignment = .center
            label.textColor = UIColor.white
            label.backgroundColor = UIColor.clear
            return label
        }()
    
        let viewForMessageLabel: UIView = {
            let view = UIView()
            view.translatesAutoresizingMaskIntoConstraints = false
            view.backgroundColor = UIColor.red
            return view
        }()
    
        fileprivate var doneButton: UIBarButtonItem?
    
        //View Controller Lifecycle
        override func viewDidLoad() {
            super.viewDidLoad()
    
            textField.delegate = self
            textView.delegate = self
    
            let tapGesture = UITapGestureRecognizer(target: self, action: #selector(removeViewForMessageLabel))
            view.addGestureRecognizer(tapGesture)
    
            textField.addTarget(self, action: #selector(removeViewForMessageLabel), for: .touchDown)
    
            addDoneButtonOnKeyboard()
        }
    
        //MARK:- Button
        @IBAction func buttonPressed(_ sender: UIButton) {
            //removeMessage()
            view.addSubview(viewForMessageLabel)
            setBackgroundAnchors()
            setMessageAndLabelAnchors()
        }
    
        //MARK:- Functions
        func setBackgroundAnchors(){
            viewForMessageLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 44).isActive = true
            viewForMessageLabel.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0).isActive = true
            viewForMessageLabel.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0).isActive = true
            viewForMessageLabel.addSubview(messagelabel)
        }
    
        func setMessageAndLabelAnchors(){
    
            messagelabel.topAnchor.constraint(equalTo: viewForMessageLabel.topAnchor, constant: 0).isActive = true
            messagelabel.widthAnchor.constraint(equalTo: viewForMessageLabel.widthAnchor).isActive = true
            viewForMessageLabel.bottomAnchor.constraint(equalTo: messagelabel.bottomAnchor, constant: 0).isActive = true
        }
    
        func removeViewForMessageLabel(){
            viewForMessageLabel.removeFromSuperview()
            textField.resignFirstResponder()
        }
    
        //MARK:- TextField Delegate
        func textFieldShouldReturn(_ textField: UITextField) -> Bool {
            view.endEditing(true)
            return true
        }
    
        func textViewDidBeginEditing(_ textView: UITextView) {
            removeViewForMessageLabel()
        }
    
        //MARK:- TextView Delegate
        func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
            if(text == "\n") {
                textView.resignFirstResponder()
                return false
            }
            return true
        }
    
        //MARK:- Additional Functions
        //add a done button to the keyboard when the textView is first responder
        fileprivate func addDoneButtonOnKeyboard(){
            let toolBar = UIToolbar()
            toolBar.sizeToFit()
            doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(dismissTextViewKeyboard))
            toolBar.setItems([doneButton!], animated: true)
            textView.inputAccessoryView = toolBar
        }
    
        //dismiss the keyboard when the Done button is tapped
        @objc func dismissTextViewKeyboard(){
            view.endEditing(true)
        }
    }