Swift indexPathsForSelectedRows returns nil - ios

I am working on a tableView, goal is to implement a expandible/collapsible tableView cells. The number of cells is completely dynamic.
I have things working, but with a small problem. I am using didSelectRowAtIndexPath to toggle expand/collapse cell. So what happens now is the toggle executes by tapping anywhere on the cell. :-(
Inside the cell, I have two UIViews (Header and Detail precisely). I need the toggle expand/collapse to work only when I tap on Header and not the whole cell.
For that, I am fetching the indexPath like here but that doesn't work since indexPathsForSelectedRows returns nil.
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return myVouchers.count
}
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if selectedIndex == indexPath.row {
return 150.0
}
else {
return 40.0
}
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
cell = tableView.dequeueReusableCellWithIdentifier("CollapsedCell", forIndexPath: indexPath) as! CollapsedCell
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action:#selector(VoucherDetailViewController.expandCell(_:)))
(cell as! CollapsedCell).headerView.addGestureRecognizer(tapGestureRecognizer)
return cell
}
func getIndexPathForSelectedCell() -> NSIndexPath? {
var indexPath: NSIndexPath?
if tableView.indexPathsForSelectedRows?.count > 0 { //**nil is returned here**
indexPath = (tableView.indexPathsForSelectedRows?[0])! as NSIndexPath
}
return indexPath
}
func expandCell(sender: UITapGestureRecognizer) {
if let myIndexPath = getIndexPathForSelectedCell() {
if selectedIndex == myIndexPath.row {
selectedIndex = -1
}
else {
selectedIndex = myIndexPath.row
}
self.tableView.beginUpdates()
self.tableView.reloadRowsAtIndexPaths([myIndexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
self.tableView.endUpdates()
}
}
Could someone give me a clue why the indexPathsForSelectedRows returns nil?
Many Thanks!
UPDATE 1:
Current Working demo
When I add my expand cell logic in didSelectRowAtIndexPath and remove my tapGestureRecognizer for the view inside the cell - it looks like this -
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
if selectedIndex == indexPath.row {
selectedIndex = -1
}
else {
selectedIndex = indexPath.row
}
self.tableView.beginUpdates()
self.tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
self.tableView.endUpdates()
}

You got this bug because
self.tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
reloads and deselect your rows.
You just select row, after that, you reload row (this action will deselect this row) and later you want to get a selected rows using
tableView.indexPathsForSelectedRows
but table view does not have any selected rows at this moment.
As a solution, you can store selected indexPath in new variable, and use this variable instead of
tableView.indexPathsForSelectedRows

From the indexPathsForSelectedRows documentation:
The value of this property is nil if there are no selected rows.
Therefore:
guard let selectedRows = tableView.indexPathsForSelectedRows else {
return nil
}
return selectedRows[0]
(no check for count is needed, the method never returns an empty array).
By the way, you could simplify the condition using:
return tableView.indexPathsForSelectedRows?.first
or, if your table does not support multiple selection, directly call:
return tableView.indexPathForSelectedRow
which is already there.

As Sulthan said try below code in place of reload
tableView.beginUpdates()
tableView.endUpdates()

Related

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

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

Troubles reloading section with more data in UITableView

I'm building an app which presents departures of busses. I use a tableview to represent it, the name of the busstop is set as the section header, and each row in a section represents a departure from that busstop.
The API always provide me with the next 20 departures for each stop, but initially I just show the next 6 departures in each section, I keep all the 20 in my datasource though. At the end of each section I have a cells which is supposed to double the shown departures in each section. It's done it this way:
tableView.beginUpdates()
tableView.reloadSections(NSIndexSet(index: indexPath.section) as IndexSet, with: .none)
tableView.endUpdates()
My initial thought was that I could just double the returnvalue of the numberOfRowsInSection-function for the specific section, but that doesn't seem to work.
var scale = [Int : Int]()
func numberOfSections(in tableView: UITableView) -> Int {
if stops.count != nil {
return stops.count
} else {
return 1
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return stops[indexPath.row].departures.count - scale[stops[section].id]
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if stops != nil {
let currentDeparture = stops[(indexPath as NSIndexPath).section].departures![(indexPath as NSIndexPath).row]
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! DepartureTableViewCell
// Configuration of the cell
return cell
}
}
return UITableViewCell()
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if indexPath.row == (stops[indexPath.row].departures.count)! {
var currentScale = scale[stops[indexPath.section].id]
scale[stops[indexPath.section].id] = currentScale - 6
tableView.beginUpdates()
tableView.reloadSections(NSIndexSet(index: indexPath.section) as IndexSet, with: .none)
tableView.endUpdates()
}
}
The dictionary scale is just mapping the ID of the stop to the number of departures that should be shown, starting at 14 (20-6) and each time the cell that is supposed to reload the section is tapped, it get reduced by 6. So we get 6 more departures in the given section. currentDepartureresInSection is the number of departures at the specific stop.
It is possible to do it this way, or do I have to update the datasource of the tableview?

Set Checkmark on view did appear using will display cell not working correctly

Here is my code for the WillDisplayRow function:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let monetaryCell = tableView.cellForRow(at: [1,0])
//configure you cell here.
if AppState.sharedInstance.filterPaymentMonetaryIsOn {
monetaryCell?.accessoryType = .checkmark
return monetaryCell!
} else {
monetaryCell?.accessoryType = .none
return monetaryCell!
}
}
And in my DidSelectRow:
override func tableView(_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath) {
let monetaryCell = tableView.cellForRow(at: [1,0])
tableView.deselectRow(at: indexPath as IndexPath, animated: true)
if indexPath == [0,0] {
print(indexPath)
} else if indexPath == [1,0] {
if AppState.sharedInstance.filterPaymentMonetaryIsOn == true {
monetaryCell!.accessoryType = .none
AppState.sharedInstance.filterPaymentMonetaryIsOn = false
} else {
monetaryCell?.accessoryType = .checkmark
AppState.sharedInstance.filterPaymentMonetaryIsOn = true
}
Can someone help me out as to why this isnt working? Also, I have alot more cells and when selected each one has the same properties as above. What I'm looking for is the checkmark to be displayed when I reload the view or close and reopen the view. I hope this makes sense. I'm sorry if this is a duplicate, I looked for a few hours but couldn't find a similar situation.
Thank you in advance.
Denis Angell
Move your check mark display functionality from cellForRowAtIndexPath method to willDisplayCell method of UITableViewDelegate. willDisplayCell method works in such a way that whenever the cell about to appear on screen willDisplayCell method calls and desired functionality executes as expected.

insertRowsAtIndexPaths in UITableView adding Duplicate Rows

I'm using the insertRowsAtIndexPaths method to add new rows to my UITableView. There is a problem of duplicate rows being added.
For example, If I type "HI" and then touch return, a row with the text "HI" is added to the table. If I then go back and type "Hello" and press return, a row that says "HI" is added again. Here is some code:
func textFieldShouldReturn(textField: UITextField) -> Bool {
sendMessage(textField.text)
return true
}
func sendMessage(text:String){
var message = Message(text: text)
//global array that stores all the messages
self.messages.append(message)
let indexPathZero : NSIndexPath = NSIndexPath(forRow: 0, inSection: 0)
self.tableView.beginUpdates()
self.tableView.insertRowsAtIndexPaths(NSArray(object: indexPathZero) as [AnyObject], withRowAnimation: UITableViewRowAnimation.None)
self.tableView.endUpdates()
}
Thanks so much!
Edit: Here's the cellForRowAtIndexPath code
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:GroupChatMessageCell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! GroupChatMessageCell
cell.message.text = messages[indexPath.row].text
cell.contentView.transform = CGAffineTransformMakeScale (1,-1);
return cell
}
Seems you are appending new messages to the end of the array, but inserting new rows at the first row. So it asks for first array element in func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
Can be fixed by changing self.messages.append(message) to self.messages.insert(message, atIndex: 0)
implement estimatedHeightForRowAtIndexPath do the trick !
-(CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
return [self tableView:tableView heightForRowAtIndexPath:indexPath];
}
Enjoy ;)

Issues with tap to expand on UITableView cell

I have a feature in my app that allows users to tap on a table view cell and it will expand to show more information. This works great but there are a few issues:
1) When I segue away from the table view, the top cell becomes expanded.
2) When I tap to expand a cell and then tap again to shrink the cell, it loses the dividing line above it until I tap a different cell.
3) When I slide to delete, the content in the expanded version of the cell overlaps the cell below it like this:
Here is my code for expanding the cell:
var selectedRowIndex: NSIndexPath = NSIndexPath(forRow: -1, inSection: 0)
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
selectedRowIndex = indexPath
tableView.beginUpdates()
tableView.endUpdates()
}
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if tableView != self.searchDisplayController?.searchResultsTableView {
if indexPath.row == selectedRowIndex.row {
if cellTapped == false {
cellTapped = true
return 141
} else {
cellTapped = false
return 68
}
}
}
return 68
}

Resources