UITableView Cell Does Not Become First Responder When Outside of Current View

Viewed 45

I have a TableViewController which displays to-do list items. In the controller I have made a button which when pressed creates a new TableViewCell at the bottom which has a UITextView along with other elements.

Till now this is what I have managed to do -

  • Create a new cell upon button tap
  • Make the newly created cell's text view first responder

However, from what I have observed everything is working fine except when the last cell in the table is not visible, i.e., it is below the frame. In that case the cell gets created but is not made the first responder or some other cell's text view gets the cursor.

See the output here - https://drive.google.com/file/d/1mRN8MEO5HBJ3ICUiRE0Yc4ib8tp62MYc/view?usp=sharing

Here is the code -

import UIKit

class TableViewController: UITableViewController, InboxCellDelegate {
        
    var cell = InboxCell()

    override func viewDidLoad() {
        super.viewDidLoad()
        // Uncomment the following line to preserve selection between presentations
        // self.clearsSelectionOnViewWillAppear = false

        // Uncomment the following line to display an Edit button in the navigation bar for this view controller.
//         self.navigationItem.leftBarButtonItem = self.editButtonItem
        
        tableView.register(UINib(nibName: "InboxCell", bundle: nil), forCellReuseIdentifier: "InboxCell")
        tableView.rowHeight = UITableView.automaticDimension
        tableView.estimatedRowHeight = 50
    }
    
    @IBAction func inboxAddPressed(_ sender: UIBarButtonItem) {
        
        addRowToEnd()
        
    }
    
    func addRowToEnd() {
        
        Task.tasks.append("")
        
        tableView.beginUpdates()
        tableView.insertRows(at: [IndexPath(row: Task.tasks.count - 1, section: 0)], with: .automatic)
        tableView.endUpdates()
        
        cell.inboxTaskTextView.becomeFirstResponder()
        
    }
    
    func didChangeText(text: String?, cell: InboxCell) {
        self.tableView.beginUpdates()
        self.tableView.endUpdates()
    }

    // MARK: - Table view data source

    override func numberOfSections(in tableView: UITableView) -> Int {
        // #warning Incomplete implementation, return the number of sections
        
        return 1
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // #warning Incomplete implementation, return the number of rows
        
        return Task.tasks.count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        cell = tableView.dequeueReusableCell(withIdentifier: "InboxCell", for: indexPath) as! InboxCell

        // Configure the cell...
        cell.delegate = self
        
        cell.inboxTaskTextView.text = Task.tasks[indexPath.row]

        return cell
    }
    
    // Override to support conditional editing of the table view.
    override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
        // Return false if you do not want the specified item to be editable.
        
        return true
    }
    
    // Override to support editing the table view.
    override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
        
        if editingStyle == .delete {
            tableView.beginUpdates()
            Task.tasks.remove(at: indexPath.row)
            tableView.deleteRows(at: [indexPath], with: .automatic)
            tableView.endUpdates()
        }
        
    }
    
    override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return UITableView.automaticDimension
    }

}

I have tried to scroll to the bottom of the table first and then making the newly created cell first responder but that didn't work. In that case only the very first cell created becomes the first responder while the subsequent cells are created but the cursor remains in the very first cell.

Here is the block of code I used for scrolling before cell.inboxTaskTextView.becomeFirstResponder() -

DispatchQueue.main.async {
    self.tableView.reloadData()
    let indexPath = IndexPath(row: Task.tasks.count - 1, section: 0)
    self.tableView.scrollToRow(at: indexPath, at: .top, animated: true)
}

Edit -

After having tried for a while this is the closest I have got to a solution.

After Task.tasks.append("") I have added the following code which scrolls down the view to the bottom -

if tableView.contentSize.height >= tableView.frame.size.height {
        DispatchQueue.main.async {
            self.tableView.reloadData()
            let indexPath = IndexPath(row: Task.tasks.count - 1, section: 0)
            self.tableView.scrollToRow(at: indexPath, at: .top, animated: true)
        }
}

In this case the newly created cell becomes first responder but only momentarily. The keyboard doesn't even appear fully before it gets dismissed automatically in a flash. This happens only for cells that are created below the fold - i.e. when the table view has to scroll down and then create a new cell.

1 Answers

Try to keep it simple first. Put a breakpoint or print("indexPath.row = \(indexPath.row)") at the beginning of cellForRow UITableViewDataSource method that you implemented already.

Add the new task and see if your cellForRow is being called for the indexPath corresponding to your new cell.

If not - you may have to scroll the tableView up at least 44points or whatever is needed to reach the area where the new cell should already be displayed. If you don't do that - the cell might not be created yet, and cell most probably refers to the last cell in the table view (or it could also be referring to a cell in the pool if some logic is not implemented correctly). So the new cell must be in the visible area before making its UITextField or UITextView become first responder.

If you know that cell is already visible - better to get an reference to it via

let index = IndexPath(row: rowForNewCell, section: 0)
let cell = self.table.cellForRow(at: index)

Finally call:

cell.inboxTaskTextView.becomeFirstResponder()
Related