UITableViewCell missing deletion control after successive left-swipes - swift

I use a tableview with editable custom style cells. There's an Edit button in the navigation bar which toggles editing mode. I need the editing mode to delete and reorder rows.
When I left-swipe a cell, the Delete action button shows on the right side of that cell. While it is displayed, I press the Edit button and the Delete action button disappears. Pressing the Edit button again shows the deletion command left and the reordering controls right of ALL(!) cells. So far all as expected.
Now (as an example), when I left-swipe cell 0, then left-swipe cell 1 while Delete action button in cell 0 is still visible, activating editing mode with Edit button now shows deletion command left and reordering control right of all cells, EXCEPT(!) cell 0.
I found out that as soon as 2 or more cells are left-wiped, the first cell which is not right-swiped back out of editing mode, misses the deletion control (left) when editing mode for all cells is enabled using Edit button. Even more weird ... on subject cell missing deletion control (left), the reordering control (right) shows correctly!
I followed and compared many tutorials but didn't catch the error.
What do I miss?
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
...
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
...
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
...
}
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
...
}
override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
...
}
override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
...
}
// not necessary = didn't change anything, but found in many tutorials
override func setEditing(_ editing: Bool, animated: Bool) {
super.setEditing(editing, animated: animated)
}
#IBAction func editAction(_ sender: Any) { // that's the Edit button action
self.setEditing(!isEditing, animated: true)
self.navigationItem.leftBarButtonItem?.title = isEditing ? "Done" : "Edit"
}
Above all involved methods. Irrelevant method content is omitted to keep the post compact. I will add/edit if deemed required.
View after left-swipe of cell 1 (disregard Rename and Share actions).
View after left-swipe of cell 2, then cell 1, then entering editing mode using Edit button.

I found the reason for the misbehaviour. Since I use animation in the setEditing() method, I need to to tell the view that there are updates using beginUpdates() and endUpdates(). That did the job! No badly displayed cells anymore neither simulator, nor device!
#IBAction func editAction(_ sender: Any) {
tableView.beginUpdates()
self.setEditing(!isEditing, animated: true)
tableView.endUpdates()
self.navigationItem.leftBarButtonItem?.title = isEditing ? "Done" : "Edit"
}
EDIT 1
Above (beginUpdates() and endUpdates()) works as desired, but below (performBatchUpdates {}) is more modern (iOS 11.0) and should be preferred.
#IBAction func editAction(_ sender: Any) {
tableView.performBatchUpdates {
self.setEditing(!isEditing, animated: true)
}
self.navigationItem.leftBarButtonItem?.title = isEditing ? "Done" : "Edit"
}
EDIT 2
Meanwhile I believe that the OP misbehaviour is an iOS bug.
By chance, I found that the Safari Bookmarks tableview behaves the same way. Left-swipe cell 0, then left-swipe cell 1, next press Done/Edit button (lower right corner) twice. You can observe that cell 0 doesn't show the deletion control icon (one-way-sign) left side of cell 0. Consequently I believe this is a iOS bug.
Besides this, other apps produce the same misbehaviour on editing tableviews.

Hi I believe your problem can be fixed by using UITableView's preset editButtonItem.
navigationItem.leftBarButtonItem = self.editButtonItem
this sets state of the button into edit mode when a swipe action is being performed, therefore should fix the issue you are having.

Related

UIContextMenu not appearing when UITableViewCell also contains a UIButton

I am creating a simple to do list app and the UITableViewCell has a checkmark button in it along with a title. When clicking the checkmark button, the done status changes and the cell UI updates.
I also would like to utilise the long press gesture within the UITableViewCell to have a ContextMenu appear. Everything seems to be working, however when a user long presses over the checkmark button, the ContextMenu doesn't appear.
I assume there are competing gestures (ie the UIButton and the cell's Long Press), but I'm having trouble figuring out how to support both at the same time. I've created the test code below to simplify the issue. In storyboard the cell has a single button on it. When long pressed, the context menu does not appear (as I need it to).
I know there's a solution somewhere!
class TableViewController: UITableViewController {
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return tableView.dequeueReusableCell(withIdentifier: "cellTest", for: indexPath)
}
override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
let actions = [UIAction(title: "Delete", image: UIImage(systemName: "trash"), attributes: .destructive, handler: { action in
return
})]
return UIContextMenuConfiguration(actionProvider: { _ in
return UIMenu(options: .displayInline, children: actions)
})
}
}

How to get didselectrowat from a custom cell with textboxes?

I have a dynamic tableview with 3(cell1,cell2,cell3) custom cells, which gets reused anywhere from 0 times up to 9 times. (Maximum cells in the tableview will be 27 cells).
Each of those 3 custom cells have 3 text boxes each, and in addition the second cell has 2 buttons to select gender and 3rd custom cell has a drop down menu, which has values of whatever typed in the first textbox of cell1 cells.
Now the issue is that, textbox takes the touch input from user (as it should) and didselectrowat never gets called and therefore i am not getting indexPath.
But i need the indexPath, so that i can insert user details from each of those cells into an array.
(anArray.insert("", at: indexPath)
Since i am not getting indexPath, everything fails. How can i get the indexPath and get the touch on to the textbox?
I am thinking of something like a function that takes the touch ,gets the indexPath and then passes the control to textbox.
Things i have tried.
I disabled the textboxes before user touches the cell (and makes textboxes disabled once user deselects the cell, so cell takes the touch. Once didselectrowat gets called, make the textbox active again and make
textbox.becomeFirstResponder()
But somehow , the textbox never becomes the first responder even though breakpoint shows the control going through it and the textbox becomes active. But no keyboard popsup, if i touch the cell again, same process happens.
What seems to be the issue here? I saw another post on the same topic, i tried it but unfortunately that doesnt work and it was for a static tableview.
Also is there a good way to do this? Other than making the user click on the cell twice / clicking outside the textbox..
You can set your custom cell as delegate for UITextField and use a callback for editing begins/ends as below,
class MyCustomCell: UITableViewCell, UITextFieldDelegate {
public var editCallback: (() -> Void)?
/// Set 'textField.delegate = self' on initialization.
func textFieldDidBeginEditing(_ textField: UITextField) {
self.editCallback?()
}
}
And use the callback in cellForRowAt as,
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = MyCustomCell()
cell.editCallback = { [weak self] in
print(indexPath)
}
return cell
}
you can try this
public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: false)
//your code here
}

"Delay Touch Down" Property Seems to be Ignored on IOS <11

I have UITableView in a test project that is created this way:
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
tableView.delegate = self
tableView.dataSource = self
}
// height?
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
//return cellHeightWithoutSeparator + cellSeparatorHeight
//print("YES!")
return 80 // arbitrary
}
// table wants a UITableViewCell for each row
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! CustomCell
// customize cell here
return cell
}
// how many rows?
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 50 // arbitrary
}
}
The structure of the table view is:
So, as you can see the cell contains a button.
I want the button (or any clickable view that is a subview of the cell) to respond immediately to the touch-down event and so I unchecked "Delay Touch Down" in the ScrollView property inspector in the storyboard.
It is my understanding that unchecking that box is equivalent to the code:
tableView.delaysContentTouches = false
According to the docs, this method is supposed to work on IOS 2.0+
But setting this property true or false only works as expected on IOS 11 and 12, but on IOS 10 (and presumably earlier), it is as if the box was never unchecked and touches on the button are still delayed.
I understand that it is still possible to make views "clickable" in the cell, and respond immediately to "touch up inside," (because the touch-up event will cancel the delay on the touch-down), but I still want the touch down event to call immediately (on all iOS versions, not just 11+) because:
1) I want the re-order control to respond immediately.
2) I want to have visual feedback for touch-down be normal/immediate on views in the cell.
I have also consulted this similar, but not the same, question which was written 4 years ago, and I tried applying the answers, but none of those answers seem to apply to whatever the modern day reason for this problem is.
Does anyone know the cause and (preferably Swift) solution to this?

Swift: How to enter edit mode for table?

I have a RootViewController that embeds a container that contains a table:
I'd like the garbage can icon on the top left of the RootViewController to enable editing mode for the embedded table view. I would like them to show up as checkboxes so that I can select multiple rows at once and then press "Delete" to delete all of the selected ones.
How would I do this?
Hopefully your class already conforms to UITableViewDelegate like so:
class MyViewController: UIViewController, UITableViewDelegate
In viewDidLoad(), you would need to have:
myTable.delegate = self
Then you can hook up the trash can icon to an IBAction that sets the table to editing mode:
#IBAction func myTableSetEditing(sender: AnyObject) {
myTable.setEditing(true, animated: true)
}
Then, as we see in an answer here: Select multiple rows in tableview and tick the selected ones, in viewDidLoad() put:
self.tableView.allowsMultipleSelection = true
and to get your checkmark, implement:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
tableView.cellForRowAtIndexPath(indexPath)?.accessoryType = UITableViewCellAccessoryType.Checkmark
}
override func tableView(tableView: UITableView, didDeselectRowAtIndexPath indexPath: NSIndexPath) {
tableView.cellForRowAtIndexPath(indexPath)?.accessoryType = UITableViewCellAccessoryType.None
}

Why can't I select TableView cells?

This is the flow of my app:
First, the TableView is set to hidden. There is a UITextField in the center of the screen. When the user types something and hits Go, this code is run:
self.view.layoutIfNeeded()
UIView.animateWithDuration(0.5, animations: {
self.textFieldConstraint.constant = -230
self.tableView.hidden = false
self.goButton.hidden = true
self.view.layoutIfNeeded()
}, completion: nil)
At this point, the tableview is populated. When a row is selected, I need to manipulate the data that is populating it.
However, absolutely nothing happens when I tap a cell.
What am I doing wrong?
My TableView code is here:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell: SearchResultsTableViewCell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! SearchResultsTableViewCell
cell.label.text = searchResultsNames[indexPath.row]
return cell
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return searchResultsUrls.count
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
print("HELLO")
}
And, I have set the dataSource and delegate properly.
I also want to clarify that the tableView populates and scrolls properly; it just won't do anything when I tap a cell.
Update:
I've discovered that for some reason, I can select the cells when I press and hold them. It is not what I want, so does anybody know how to fix this?
I have just used your code to create a simple table, selection is working fine and logging out HELLO as expected. Can you check the values of Selection in the attributes inspector? Here is mine, which has Selection set to Single Selection.
And here is the code I used for my simple table
import UIKit
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var tableView: UITableView!
var searchResults = [String]()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
searchResults.append("Testing 1")
searchResults.append("Testing 2")
searchResults.append("Testing 3")
searchResults.append("Testing 4")
searchResults.append("Testing 5")
tableView.dataSource = self
tableView.delegate = self
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("tableCell", forIndexPath: indexPath)
cell.textLabel?.text = searchResults[indexPath.row]
return cell
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return searchResults.count
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
print("HELLO")
}
}
I also tried hiding and showing the tableView which made no difference on selection.
EDIT & SOLUTION:
In the comments below, we discovered that the issue is related to a tapGestureRecogniser on the view, This was identified by the op only being able to make a selection by holding a tap on the cell. The gesture has to fail before the selection can be made, the op managed to solve the problem by referring to this other SO Answer
if you use "tap" gesture, you can't select table cell. (but, if you click and drag to right a cell, you can select it.)
Check gesture first.
And if your code has self.tableView.allowsSelection = false, replace false to true or delete this line.
My problem has been caused by the tap gesture recognizer on the view controller itself (I had BaseTableViewController I was extending from). Probably it was interfering with the gesture recognizers of UITableView.
In your viewDidLoad, or wherever you set up your view, make certain that your table view even allows selections. That can be controlled with the allowSelection property.
Something like:
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.allowsSelection = true
}
If you have a gesture recognizer, just type gestureRecognizer.cancelsTouchesInView = false
For me I was implementing another did select row method, so I erased it and typed "didSelect..." and selected the first one in the suggested methods, which is this for swift 3:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("Row #: \(indexPath)")
}
Try disabling and then enabling the user interaction Enabled property in the attribute inspector of your tableView
Looks something like this
I met the same problem as you and solved it by removing the below code
self.view.layoutIfNeeded()
you can try it.