iosswiftuitableviewtextviewfirst-responder

How to prevent the keyboard from lowering when a new UITextView is assigned as first responder


I have a table view that utilizes custom cells with UITextViews. Whenever a user is editing the text in a cell's textView and then hits return, a new cell is inserted to the list of data that populates the table view cells, and tableView.reloadData() is called so that the new cell shows up immediately. The textView.tag + 1 of the cell that was being edited when the user pressed return is stored as a variable called cellCreatedWithReturn, and if that variable is not nil when tableView is reloaded, the cell with that indexPath.row (so the new cell that was just created) becomes the first responder.

The issue I'm having is that when I hit return, the new cell is created and it is assigned as first responder, but the keyboard spazzes out because it starts to go into hiding and then shoots back up, instead of just staying put. An app that demonstrates the functionality I'm looking for would be Apple's Reminders app. When you hit return, a new cell is created and editing begins on that new cell, but the keyboard stays up the whole time without spazzing.

One thing I tried was commenting out the textView.endEditing(true) from my shouldChangeTextIn function to see if that was the cause of the keyboard being lowered, but this resulted in no change.

Here are my shouldChangeTextIn and cellForRowAt functions:

var cellCreatedWithReturn: Int?

func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
        if(text == "\n") {
            textView.endEditing(true)
            cellCreatedWithReturn = textView.tag + 1
            if song.lyrics.count == textView.tag || song.lyrics[textView.tag].text != "" {
                let newLyricLine = LyricLine()
                newLyricLine.text = ""
                do {
                    try realm.write {
                        self.song.lyrics.insert(newLyricLine, at: textView.tag)
                        print("Successfully inserted new lyric line in Realm")
                    }
                } catch {
                    print("Error when inserting new lyric line after pressing return")
                }
            }
            tableView.reloadData()
            return false
        } else {
            return true
        }
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "lyricsCell", for: indexPath) as! newNoteTableViewCell
        
        cell.lyricsField.delegate = self
        
        DispatchQueue.main.async {
            if let newCellIndexPath = self.cellCreatedWithReturn {
                if indexPath.row == newCellIndexPath {
                    cell.lyricsField.becomeFirstResponder()
                }
            }
        }

}

Solution

  • First, handle your text view actions inside your cell class. Then use closures to tell the controller what has happened.

    So, when the user taps Return:

    Here's a very simple example:

    // simple cell with a text view
    class TextViewCell: UITableViewCell, UITextViewDelegate {
        
        var textView = UITextView()
        
        // closure to tell controller Return was tapped
        var returnKeyCallback: (()->())?
        
        // closure to tell controller text was changed (edited)
        var changedCallback: ((String)->())?
        
        override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
            super.init(style: style, reuseIdentifier: reuseIdentifier)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        func commonInit() -> Void {
            textView.translatesAutoresizingMaskIntoConstraints = false
            contentView.addSubview(textView)
            let g = contentView.layoutMarginsGuide
            NSLayoutConstraint.activate([
                
                textView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
                textView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
                textView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
                
                // use lessThanOrEqualTo for bottom anchor to prevent auto-layout complaints
                textView.bottomAnchor.constraint(lessThanOrEqualTo: g.bottomAnchor, constant: 0.0),
                
                textView.heightAnchor.constraint(equalToConstant: 60.0),
    
            ])
            
            textView.delegate = self
            
            // so we can see the text view frame
            textView.backgroundColor = .yellow
        }
        
        func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
            if(text == "\n") {
                returnKeyCallback?()
                return false
            }
            return true
        }
        func textViewDidChange(_ textView: UITextView) {
            let t = textView.text ?? ""
            changedCallback?(t)
        }
            
    }
    
    class AnExampleTableViewController: UITableViewController {
        
        // start with one "row" of string data
        var theData: [String] = [ "First row" ]
        
        override func viewDidLoad() {
            super.viewDidLoad()
            tableView.register(TextViewCell.self, forCellReuseIdentifier: "TextViewCell")
        }
        
        override func numberOfSections(in tableView: UITableView) -> Int {
            return 1
        }
        override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return theData.count
        }
        override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let c = tableView.dequeueReusableCell(withIdentifier: "TextViewCell", for: indexPath) as! TextViewCell
            
            c.textView.text = theData[indexPath.row]
            
            // handle Return key in text view in cell
            c.returnKeyCallback = { [weak self] in
                if let self = self {
                    let newRow = indexPath.row + 1
                    self.theData.insert("", at: newRow)
                    let newIndexPath = IndexPath(row: newRow, section: 0)
                    self.tableView.performBatchUpdates({
                        self.tableView.insertRows(at: [newIndexPath], with: .automatic)
                    }, completion: { b in
                        guard let c = tableView.cellForRow(at: newIndexPath) as? TextViewCell else { return }
                        c.textView.becomeFirstResponder()
                    })
                }
            }
            
            // update data whenever text in cell is changed (edited)
            c.changedCallback = { [weak self] str in
                if let self = self {
                    self.theData[indexPath.row] = str
                }
            }
            
            return c
        }
        
    }