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)
}
}
}
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 table view cell that has a button which, when clicked, shows an action view.
After reading this answer saying that the logic to show the action view should be handled in the controller.
The problem is that I use these table view cells in several different controllers and it seems counterintuitive to copy and paste the action view logic into each controller where the table view cells exist, especially considering the maintenance required.
So my question is, what is the best approach to handling this? Is there a way that I can handle all of the action view logic in a single place, and refer the controllers to that code?
Have the action for click in tableview cell and pass a variable in cellforrow so you know from which screen you are coming and accordingly handle the click.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell : RiderCell! = tableView.dequeueReusableCell( withIdentifier: "RiderCell") as! RiderCell
cell.UpdateCell(from:"Profile")
cell.selectionStyle = .none
return cell as RiderCell
}
in you table cell
import UIKit
class RiderCell: UITableViewCell {
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
func UpdateCell(from:String){
//manage click here
}
}
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 tableView with several section and each section contains only one cell (this is done to create a gab between each cell). Im trying to create a custom selection view for my cell when it is selected.
when I select a row, the custom selection view is being added to more than one cell. I know the problem is because cells are being reused. What is the best suitable solution to overcome this problem?
This is my code.
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
if let myCell = tableView.cellForRowAtIndexPath(indexPath) as? PredefinedServicesCell{
let selectionView = UIView()
selectionView.backgroundColor = UIColor(hex: 0x3399CC).colorWithAlphaComponent(0.2)
selectionView.layer.cornerRadius = (myCell.containerView.layer.cornerRadius)
selectionView.frame = (myCell.containerView.frame)
myCell.containerView.addSubview(selectionView)
}
}
You can make you custom selected view hide or show in the following method in your CustomCell Class
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
self.customSelectedView.hidden = !selected
}
I have a static cell and when clicked it launches a modal view. Except when i return from the modal view the cell is still selected? Why is it doing this and how can I make it only make the cell selected until the modal completely covers the view.
Thanks in advance
For Swift 2 :
override func viewWillAppear(animated: Bool) {
if let indexPath = self.tableView.indexPathForSelectedRow {
tableView.deselectRowAtIndexPath(indexPath, animated: true)
}
}
For Swift 3 and 4 :
override func viewWillAppear(animated: Bool) {
if let indexPath = self.tableView.indexPathForSelectedRow {
tableView.deselectRow(at: indexPath, animated: true)
}
}
If you use a UITableViewController instead of a UIViewController this will be done automatically. Otherwise, you need to do the deselecting yourself using
deselectRowAtIndexPath:animated: on the UITableView. The best place to do this is probably on viewDidAppear: of the presenting view controller. That way, the user still sees the deselecting animation allowing them to reorient themselves.
If you don't need to track the selected row for other purposes, you can use
indexPathForSelectedRow to determine which index path needs to be deselected (if any).
I know this is too late but may help someone who's using Swift -
This will give a nice effect when you return to masterViewController from detailViewController
override func viewWillAppear(animated: Bool)
{
if let indexPath = self.tableView.indexPathForSelectedRow
{
self.tableView.deselectRowAtIndexPath(indexPath, animated: true)
}
}
Otherwise add this delegate method of TableView and call it from didSelectRowAtIndexPath
func tableView(tableView: UITableView, didDeselectRowAtIndexPath indexPath: NSIndexPath)
{
if let indexPath = tableView.indexPathForSelectedRow
{
tableView.deselectRowAtIndexPath(indexPath, animated: true)
}
}
You can use this in your UITableViewController;
- (void)viewWillAppear:(BOOL)animated
{
[yourTableView deselectRowAtIndexPath:[yourTableView indexPathForSelectedRow] animated:YES];
}
In your condition, yourTableView property should be self.tableView
swift 3
override func viewWillAppear(animated: Bool) {
tableView.deselectRow(at: tableView.indexPathForSelectedRow!, animated: true)
}