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

229 views Asked by At

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()
                }
            }
        }

}
1

There are 1 answers

2
DonMag On BEST ANSWER

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:

  • Intercept it in shouldChangeTextIn
  • Use a closure to inform the controller
  • In your controller, add an element to your data structure
  • Use .performBatchUpdates() to insert a cell at the next row in your table view
  • On completion, tell the text view in the new cell to .becomeFirstResponder()

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
    }
    
}