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.
Related
I have a view controller with a table view embedded inside it, although it is always the same ten cells, I made it dynamic as you cannot make static table view cell inside a UIViewController. My question is, in prepare(for:) how can I segue to a view controller based on which cell was tapped? Each cell should navigatie to a completely different VC, but I think having ten segues is a mess.
Is there a better way?
You can implement the UITableViewDelegate protocol to actually detect the cell being tapped:
extension MyOriginViewController {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// I'm assuming you have only 1 section
switch indexPath.row {
case 0:
// Instantiate the VC
let vc = storyboard?.instantiateViewController(withIdentifier: "MyViewControllerIdentifier")
// Present the view controller, I'm using a UINavigationController for that
navigationController?.push(vc, animated: true)
case 1:
// Another VC and push or present modally
let vc = ...
self.present(vc, animated: true, completion: nil)
default:
// Default case (unconsidered index)
print("Do something else")
}
}
}
So if you have this:
You can do:
let vc = storyboard?.instantiateViewController(withIdentifier: "MyNavController")
self.navigationController?.present(vc, animated: true, completion: nil)
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 list with clickable cells, when one is clicked a new viewcontroller opens up. When a back button is clicked and the first VC is called, the tableview resets to the top of the list. How can I change this so when the back button is clicked the tableview goes back to the original cell clicked? From what I understand, I need a tableview.scrollToRow, but I'm getting a little lost in the indexPath that I need to select (believe I need to save the last selected row, but now sure how to do this)
Here's the code:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let webVC = UIStoryboard.init(name: "MainVC", bundle: nil).instantiateViewController(withIdentifier: "SecondaryVC") as! WebViewController
webVC.urlLink = self.listings?[indexPath.row].url
self.present(webVC, animated: true, completion: nil) }
override func viewWillAppear(_ animated: Bool) {
tableview.scrollToRow(at: indexPathSelected, at: .middle, animated: false)
}
Your problem is that you are actually moving forward to a new instance of your view controller rather than back to the existing instance. If you move back then the state of the view controller will be as you left it and there will be no need to do anything to the tableview.
You should use an unwind segue to move back
As your cell is respond to your touch(and you did not call deselectRow(at indexPath: IndexPath) after selecting cell), the tableView will keep your selection(s), so when you back to your viewController just call tableView.indexPathForSelectedRow(or tableView.indexPathsForSelectedRows) in your viewWillAppear() method.
If I side swipe on a table view row to delete it then the row does not dismiss as a complete entity - by this I mean the edit action portion of the cell always dismisses itself by sliding upwards, but the rest of the cell dismisses itself according to whatever value has been set for UITableViewRowAnimation.
So for example if the row below is deleted using deleteRows(... .left) then the white portion of the row will slide off to the left of the screen but the UNBLOCK part behaves separately - it always slides up the screen, irrespective of the UITableViewRowAnimation value used in deleteRows.
Code is as follows:
override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
let deleteAction = UITableViewRowAction(style: .default, title: "UNBLOCK") { action, index in
self.lastSelectedRow = indexPath
let callerToUnblock = self.blockedList[indexPath.row]
self.unblockCaller(caller: callerToUnblock)
}
deleteAction.backgroundColor = UIColor.tableViewRowDeleteColor()
return [deleteAction]
}
func unblockCaller(caller:Caller) {
DispatchQueue.global(qos: .background).async {
Model.singleton().unblockCaller(caller) {(error, returnedCaller) -> Void in
if error == nil {
DispatchQueue.main.async {
if let lastSelectedRow = self.lastSelectedRow {
self.tableView.deleteRows(at: [lastSelectedRow], with: .left)
}
}
...
Does anybody know how to make the whole row move consistently together?
Correct. These two objects behave as separate entities. You cannot control the animation of the UITableViewRowAction button as it drawn outside of the UITableViewCell frame.
See the UITableViewCellDeleteConfirmationView layout in this Captured View Hierarchy. (Notice how the UITableViewCell frame, in blue, doesn't encompass the UITableViewCellDeleteConfirmationView, in red):
Fix
Hide the editing button prior deleting the cell. It will be erased prior the cell deletion animation, before the rest of the cells scroll upward.
func unblockCaller(caller:Caller, indexPath:IndexPath) {
// Perform the deletion
// For example: self.blockedList.remove(at: indexPath.row)
// if error == nil...
DispatchQueue.main.async {
self.tableView.setEditing(false, animated: false)
self.tableView.deleteRows(at: [indexPath], with: .left)
}
}
Additional Comments
I recommend not using state variables to keep track of which cell must be deleted (self.lastSelectedRow = indexPath). Pass the path to the worker method instead for a better isolation of the tasks.
Use the standard appearance whenever possible ; if the UNBLOCK button is but a red control, prefer the style: .destructive and avoid custom color schemes. The editActionsForRowAt becomes a single line:
self.unblockCaller(caller: self.blockedList[indexPath.row], indexPath: indexPath)
I'm having a problem with my tableview, I can tap on my cells just fine. But if I quickly tap the same cell twice. It runs the "didSelectRowAtIndexPath" twice. Adding 2 of the same views, to my navigation Controller Stack
Here's the function:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
var VC = detaiLSuggestion_VC()
self.navigationController?.pushViewController(VC, animated: true)
var selectedCell:UITableViewCell = tableView.cellForRowAtIndexPath(indexPath)!
if selectedCell.backgroundColor == UIColor.formulaFormColor() {
println("Buy Action")
selectedCell.backgroundColor = UIColor.formulaMediumBlue()
UIView.animateWithDuration(0.5, animations: {
selectedCell.backgroundColor = UIColor.formulaFormColor()
})
} else {
println("Sell Action")
selectedCell.backgroundColor = UIColor.formulaGreenColor()
UIView.animateWithDuration(0.5, animations: {
selectedCell.backgroundColor = UIColor.formulaLightGreenColor()
})
}
why am I able to select the same cell twice? and how would I go about fixing that?
why am I able to select the same cell twice?
Because the cell is still on screen to be tapped. You might want to do something other than navigation with cell taps and then you would not want to block future taps. Also multiselect would be much more complicated if this was not the case.
and how would I go about fixing that?
A simple boolean flag to indicate that something has been tapped would work or else you might want to set the allowsSelection property of the CollectionView to NO while you are presenting another view controller