I can see this behavior in both deprecated UITableViewRowAction class and UISwipeActionsConfiguration class:
If you have allowsMultipleSelection property set to true and, let's say, you have 3 rows selected:
When you start swiping any row in the table for a RowAction the previously selected rows -- all 3 of them -- become unhighlighted, and the property indexPathsForSelectedRows drops to nil.
Does this behavior make sense?
Is there any 'deselecting' callback (because I'm displaying the number of selected rows)
What are possible workarounds to persist the array of selected rows?
UITableView enters editing mode when you swipe a row in the table. This is your
'deselecting' callback
You can backup your selected rows on entering the mode and restore on exiting:
class ViewController: UITableViewController {
var indexPathsForSelectedRows: [IndexPath]?
override func setEditing(_ editing: Bool, animated: Bool) {
if editing {
indexPathsForSelectedRows = tableView.indexPathsForSelectedRows
} else {
indexPathsForSelectedRows?.forEach { tableView.selectRow(at: $0, animated: false, scrollPosition: .none) }
}
super.setEditing(editing, animated: animated)
}
}
Also note that if you re-arrange/delete/insert rows during editing, you'll need to update your stored indexPathsForSelectedRows accordingly so you restore correct index paths.
Related
After we tap on the table view cells to push and pop to the detail view, if we swipe back to the previous table view, you'll notice that the cell stays highlighted and interactively unhighlights as we swipe.
How can this be programmatically implemented in UIKit?
The following reference illustrates the behaviour:
WWDC20 Introduction to SwiftUI: https://developer.apple.com/videos/play/wwdc2020-10119/?time=630
First, if you haven't already, you need to mark your "selected" when you tap on it:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let cell = tableView.cellForRow(at: indexPath) as? SubclassedCell else {
return
}
//`setSelected(:animated:) is built into `UITableViewCell`
cell.setSelected(true, animated: true)
...
Then in viewWillAppear(_:) you're going to coordinate the deselection animation with the edge swipe animation:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
//1
guard let selectedIndexPath = tableView.indexPathForSelectedRow else {
return
}
//2
if let transitionCoordinator = self.transitionCoordinator {
transitionCoordinator.animate(alongsideTransition: { (context) in
self.tableView.deselectRow(at: selectedIndexPath, animated: true)
}, completion: nil)
//3
transitionCoordinator.notifyWhenInteractionChanges { (context) in
if context.isCancelled {
self.tableView.selectRow(at: selectedIndexPath, animated: true, scrollPosition: .none)
}
}
} else {
//4
tableView.deselectRow(at: selectedIndexPath, animated: animated)
}
}
That's a LOT of code, here are the highlights:
Only run this if there's a selected index path. There's no selection if you’re on this screen for the first time. (Btw, the table view keeps track of its own selected index path(s). You just need to mark cells selected or not selected).
Coordinate the row deselection animation with the current animation "context" (i.e. the edge swipe animation context).
You might change your mind mid-swipe! If this happens, you want to re-select the thing you were deselecting.
Back in the day, before transition coordinators, you only had to add this one line. This else case is there in case there's no transition coordinator (old version of iOS, going back in the stack without animation, etc).
Ok..before you give up on UIKit, know there's a shortcut.
Shortcut: Use UITableViewController instead of UIViewController
Instead of subclassing UIViewController and adding a table view, just subclass UITableViewController. You still have to mark your cell selected, but that's it.
This works because UITableViewController has a property called clearsSelectionOnViewWillAppear, which is set to true by default. It takes care of everything for you.
I have a TableView with a playlist of music tracks. When I click on the row, the track starts to play and the current row is selected. Also, I have a search bar, which works fine. But if I click on the row, then on the search bar and then I finally on the Cancel button - the row with the current track is not selected. In other situations, like forward or backward playback, the selection of rows works well.
Simplified, my code looks like this:
class TableView: UIViewController, UISearchResaultUpdating, UISearchBarDelegate {
...
private var currentSelectedRowIndex = 0
func ...didSelectRowAt indexPath... {
...
currentSelectedRowIndex = indexPath.row
print(currentSelectedRowIndex)
}
...
func searchCancelButtonClicked... {
print("Cancel button pressed")
print(currentSelectedRowIndex)
tableView.selectRow(at: IndexPath(row: currentSelectedRowIndex, section 0), animated: true, scrollPosition: .middle)
}
}
//For example, I pressed second row
Console message:
1
Cancel button pressed
1
==========================
But the row is not selected, what's the problem?
If you look at the UITableView documentation, it clearly says that calling selectRow does not cause the delegate to receive tableView(_:didSelectRowAt:).
You can move whatever you are doing in tableView(_:didSelectRowAt:) to another function and call that function in tableView(_:didSelectRowAt:) and searchCancelButtonClicked.
Or you can use the following code to manually invoke tableView(_:didSelectRowAt:) in searchCancelButtonClicked.
func searchCancelButtonClicked() {
print("Cancel button pressed")
print(currentSelectedRowIndex)
let indexPath = IndexPath(row: currentSelectedRowIndex, section: 0)
yourTableViewOutlet.delegate?.tableView?(yourTableViewOutlet, didSelectRowAt: indexPath)
tableView.selectRow(at: indexPath, animated: true, scrollPosition: .middle)
}
I have the common pattern of a UITableView with a secondary view controller which gets pushed over the top when a row is selected. To give the user some context when they dismiss the second view controller and return to the tableview, that first view controller has this:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if let index = tableView.indexPathForSelectedRow {
tableView.deselectRow(at: index, animated: animated)
}
}
That results in this unintended and jarring transition in which the cell being deselected fades it's background away, before snapping back to normal :
My expectation was that it would transition from the partially subdued state selection left it in directly back to the normal, dark state.
(The cell is very much a work-in-progress - it's far from finished)
Following the suggestions here isn't really an option as I do want to preserve the context hint and the cell as a whole should continue to have a white background.
In response to Rico's question, the cell is created as a .swift and .xib pair, the hierarchy of views being:
The Swift does very little - sets .textInsets on the two labels, draws the disclosure indicator in the button.
I believe this is because the default implementation of setSelected removes the background color of all subviews of the cell. What you can do is override setSelected and/or setHighlighted and update the cell yourself.
This also allows you to create a custom look for selected cells.
Example that uses a red background when selected, and white when not selected:
override func setSelected(_ selected: Bool, animated: Bool) {
let animationDuration = animated ? 0.3 : 0
UIView.animate(withDuration: animationDuration) {
self.backgroundColor = highlighted ? UIColor.red : UIColor.white
}
}
Instead of deselecting the row in viewWillAppear(_:), call deselectRow(at:animated:) inside the tableView(_:didSelectRowAt:) method, i.e
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
//Your rest of the code...
}
Edit-1:
In viewWillAppear(_:), you need to deselect the cell in UIView.animate(withDuration:animations:) with animation set to true in deselectRow(at:animated:), i.e.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
UIView.animate(withDuration: 0.2) {
if let indexPath = self.tableView.indexPathForSelectedRow {
self.tableView.deselectRow(at: indexPath, animated: true)
}
}
}
I have a tableview cell shown like this…
In the above tableview, I am showing an accessory type checkmark for selection. For that I have set this in the UITableViewCell class…
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
if selected {
editingAccessoryType = .checkmark
} else {
editingAccessoryType = .none
}
}
But this pushes my tableviewcell to the left like so...
How can I avoid this and have the accessory type checkmark appear within the cell itself...?
you can put your cell background to white instead of your view so this can appear as you want.
I have a master detail layout with table views in both of the views. Left panel has a list of patients and right has a list of documents, what happens is by changing the patient selection documents will be reloaded and if the user clicks on a document, it will segue to a webView to display PDF.
For consistency purpose i made the first cell in the Patient table to be selected by default using the below code
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(true)
let rowToSelect:NSIndexPath = NSIndexPath(forRow: 0, inSection: 0);
self.tableView.selectRowAtIndexPath(rowToSelect, animated: true, scrollPosition: UITableViewScrollPosition.None)
self.performSegueWithIdentifier("showDetail", sender: self)
}
work's fine right?, Yes but it gets a bit inconsistent from here, after closing this PDF document (by clicking done on navigation), my Previously selected cell will go off and it again points to the first Patient. Is there any way to save this selection? Oh and even to save the selection on the details page too. Thanks.
Create an NSIndexPath property.
Change your viewDidAppear to only default-select the first cell if there wasn't something previously selected:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(true)
if (!self.rowToSelect) {
rowToSelect = NSIndexPath(forRow: 0, inSection: 0);
self.tableView.selectRowAtIndexPath(rowToSelect, animated: true, scrollPosition: UITableViewScrollPosition.None)
self.performSegueWithIdentifier("showDetail", sender: self)
}
}
In the didSelectRow delegate method store the selected index path in self.rowToSelect.