The following test app displays a simple tableView. The property selectionStyle of the prototype cell is set to default (grey). Since there are more cells than can be displayed, the first cells are visible, the last cells not. The background color of each cell is set to white.
First test:
When one of the cells is selected, its background becomes grey.
In the delegate function, its isSelected property is then set to false, thus the cells background becomes white again. The cell is then moved to row 0, and the data source is updated accordingly.
This works as expected.
Second test:
The tableView is scrolled up so that the last cells become visible.
When a cell is now selected, it is moved again to row 0, which has been moved out of the visible area of the tableView. This works again. However:
If the tableView is then scrolled down so that row 0 becomes visible again, the moved cell has now a grey background, as if it were selected, but it is not. This does not work as expected:
Here is my code:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
var tableData = ["00", "01", "02", "03", "04", "05", "06", "07", "08", "09",
"10", "11", "12", "13", "14", "15", "16", "17", "18", "19"]
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tableData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell")!
let row = indexPath.row
cell.textLabel?.text = tableData[row]
cell.backgroundColor = .white
return cell
}
}
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let selectedCell = tableView.cellForRow(at: indexPath)!
selectedCell.isSelected = false
let sourceRow = indexPath.row
let destinationRow = 0
let removedElement = tableData.remove(at: sourceRow)
tableData.insert(removedElement, at: 0)
let destinationIndexPath = IndexPath.init(row: destinationRow, section: 0)
tableView.moveRow(at: indexPath, to: destinationIndexPath)
}
}
My question:
Is anything wrong with my code?
Or is this an iOS bug? If so, is there a workaround?
Please note:
It is of course possible to use tableView.reloadData() instead of tableView.moveRow(at:, to:). Then, the moved cell has no grey background. But in this case, the movement is not animated, as I need it in my app under development.
If property selectionStyle of the prototype cell is set to none, the moved cell has neither a grey background, but then one has no visible feedback of the selection.
Instead of set the isSelected property, call the deselectRow(at:animated:) method of UITableView:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
...
}
I submitted a bug report to Apple and received the following answer:
You should generally always use the deselectRow(at:animated:) method
on UITableView to deselect a row. If you only change the selected
property on the UITableViewCell currently shown for that row directly,
the table view will not be made aware of this, and when a new cell is
reused for that same row during scrolling, the table view will reset
the selected state of the cell to match whether it thinks the row is
selected. The table view’s state is always the source of truth; the
cell state is simply the current visual representation for a
particular row.
Good to know!
Related
I have an application that is viewbased and I am adding a uitableview as a subview to the main view. The uitableview is full with class Category cells. Everything works fine, but I want to have “Quick notes” Category always on the top of the uitableview. This means when I reloadData() in the Array, “Quick Notes” is always with index 0 and it goes on the bottom of the uitableview. And when I create new cell I need it to go under the “Quick Notes” section.
Please help me, what code I need to achieve that functionality and where I need to put it. Thanks!
Edit:
Thats where I am adding "Quick Notes" to the Realm database.
private let categories = try! Realm()
private init() {
if categories.objects(Category.self).isEmpty {
createCategoryWith(title: "Quick Notes", color: "#FF0000", icon: "quickNotes")
}
}
Update the array in the ViewController:
func didCreateCategory(category: Category) {
RealmHandler.shared.createCategoryWith(title: category.title, color: category.color, icon: category.icon)
self.categories = RealmHandler.shared.getAllCategories()
tableView.reloadData()
}
DataSourceDelegate:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "categoryCell", for: indexPath)
cell.textLabel?.text = categories[categories.count - (1+indexPath.row)].getTitle()
cell.contentView.backgroundColor = hexStringToUIColor(hex: categories[categories.count-(1+indexPath.row)].getColor())
return cell
}
use viewForHeaderInSection delegate method of tableView.
tableView(_ tableView: UITableView, viewForHeaderInSection section: Int)
here you can define your header cell.
then implement your logic that what do you want to show on this cell.
this headerCell will be always on top of your tableView.
I have default UITableViewController with 10 row
have UITableViewCell with segment control
If I change segmentController selected index in probably cell : 2, then scroll down this state of segmentController will be in other cells
cells1(selectedIndex: 1 )
cells2(selectedIndex: 1 )
cells3(selectedIndex: 1 )
cells4(selectedIndex: 1 )
cells5(selectedIndex: 1 )
... Select in cell 2 other index (2)
scroll down
and cell 5 have same selected index, but I am not touch them
cells1(selectedIndex: 1 )
cells2(selectedIndex: 2 )
cells3(selectedIndex: 1 )
cells4(selectedIndex: 1 )
cells5(selectedIndex: 2 )
selected 0 row
row 5 is too selected, why?
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.selectionStyle = .none
cell.textLabel?.text = String(indexPath.row)
cell.textLabel?.textColor = .white
return cell
}
UITableView and UICollectionView reuse cells. Which means it only keeps enough cells in memory to show what is on the screen. As you scroll cells off the screen they are fed back in to become the cells that are appearing. You need to properly configure the cells as they re-appear in your override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)
So in your case you need to set the segmented control's selection in cellForRow every time.
This means you will need a way to keep track of what selection has been made on your segmented controls and call that up to set the segmented control correctly when the cell is being shown again.
Reset selection in prepareForReuse method of UITableViewCell by subclassing it.
override func prepareForReuse() {
}
I am having problems selecting the right cells within my table view. My code is:
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var MyTableView: UITableView
var cellsArray = [ "0" , "1" , "2" , "3" , "4" , "5" , "6" , "7"]
override func viewDidLoad() {
super.viewDidLoad()
MyTableView.layer.borderWidth = 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 8
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = MyTableView.dequeueReusableCell(withIdentifier: "cells", for: indexPath)
cell.textLabel?.text = cellsArray[indexPath.row]
return cell
}
#IBAction func MyButtonClick(_ sender: Any) {
let myIndexPath = IndexPath(row: 4 ,section: 0)
let cell = MyTableView.cellForRow(at: myIndexPath)
cell?.backgroundColor = UIColor.yellow
}
This is the application when it's running
The problem is when I press the button without scrolling the tableview nothing happens, but if I scroll down so the current view includes cells labeled 4/5/6 and press the button both cells labeled "4" and "0" have their background colors set to yellow.
I would ultimately like to know why this is the case since it's been effecting more than just background, like when doing a for loop to sum the cell heights to auto change the height of the tableview, the cells not in view crash the program as it's returning null.
Any help would be greatly appreciated on why is this!
After button press when 4 is in view
Cells are being reused - you can only get reference to the visible cells.
let myIndexPath = IndexPath(row: 4 ,section: 0)
let cell = MyTableView.cellForRow(at: myIndexPath)
cell?.backgroundColor = UIColor.yellow
cell in your case is nil when row number 4 isn't visible. If you wanna change behaviour in the cells you should modify the model and call for example reloadData on your UITableView.
I want to do something like in the GIF
I tried 2 ways, one was hiding the elements on selecting the row and showing others, but that's not really elegant and doesn't work very well
and second was creating 2 views, one with labels, another with buttons, adding them as subviews to cell.contentView but that caused some issues with other cells as they were displaying wrong data. How can I recreate something like this?
I think something like this would work:
Use 2 different UITableViewCells: add them to the table view in your storyboard and design them separately, also you can use 2 different UITableViewCell subclasses for them
Have an array in the tableview's datasource class that will define the type of the cell from each row (e.g. the simplest solution would be an array of integers, with values: 0 representing the first cell, 1 representing the second cell)
Initialise that array with 0s for each row
In tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell :
if cellTypes[indexPath.row] == 0 --> return a cell of first type
if cellTypes[indexPath.row] == 1 --> return a cell of the second type
In tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) :
switch the cell type in the array
reload the row with animation, e.g. you can use .fade or .left or .right etc.
tableView.reloadRows(at: [indexPath], with: .fade)
EDIT: Your solution is also a good one, but it can cause problems when the cells are dequeued, so if a cell with the wrong subviews is dequeued then you need to switch the subviews back in the cellForRowAt indexPath datasource method.
EDIT2: I took some time and I have tried my solution in Xcode. Here is the code of my tableview controller:
class TableViewController: UITableViewController {
private var cellTypes: [Int] = [1, 1, 1, 1, 1, 1]
public override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.cellTypes.count
}
public override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 56.0
}
public override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if self.cellTypes[indexPath.row] == 1 {
return tableView.dequeueReusableCell(withIdentifier: "Cell1", for: indexPath)[![enter image description here][1]][1]
} else {
return tableView.dequeueReusableCell(withIdentifier: "Cell2", for: indexPath)
}
}
public override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
if self.cellTypes[indexPath.row] == 1 {
self.cellTypes[indexPath.row] = 2
tableView.reloadRows(at: [indexPath], with: .fade)
} else {
self.cellTypes[indexPath.row] = 1
tableView.reloadRows(at: [indexPath], with: .right)
}
}
}
And here is how it is working in the iOS simulator:
I believe you are on the right track about creating 2 separate views inside the cell; one for showing 3 buttons (Play Now, Play Next etc.) and, one for showing the song's details (song name, singer name etc.).
In order not to mess with frames or constraints (in case you are using Autolayout), the main trick here is to create a snapshot of the view containing the buttons and move it to the end of the cell.
As I said above, you should have 2 separate views. I'll call them:
infoView: View that has 2 labels showing the song's and the singer's name.
actionsView: View that has 3 buttons for play actions. (Now, Next, Last etc.)
Here are things that you should do when user taps on a cell:
Check if cell is not selected. If it is not, then hide infoView and show actionView.
If cell is selected:
Deselect the cell.
Create a snapshot out of actionsView, set its frame accordingly so it'll shadow the real actionsView.
Set actionView's isHidden property to true.
Set infoView's isHidden property to false.
Set frame.origin.x value of the snapshot to contentView's maxX in an animation block so it'll move to the right side of the cell smoothly.
At the end of the animation, remove the snapshot from view hierarchy.
I've created a cell class and defined a method that executes those steps:
public class SongCell: UITableViewCell {
#IBOutlet fileprivate weak var infoView: UIView!
#IBOutlet fileprivate weak var actionsView: UIView!
...
public func showActions(_ show: Bool) {
switch show {
case true:
infoView.isHidden = true
actionsView.isHidden = false
case false:
if let snapshot = actionsView.snapshotView(afterScreenUpdates: true) {
snapshot.frame = actionsView.frame
contentView.addSubview(snapshot)
actionsView.isHidden = true
infoView.isHidden = false
UIView.animate(withDuration: 0.25, animations: {
snapshot.frame.origin.x = self.contentView.frame.maxX
}, completion: { _ in
snapshot.removeFromSuperview()
})
}
else {
infoView.isHidden = false
actionsView.isHidden = true
}
}
}
}
Here is how it looks on my simulator:
You can download the project from here.
I have a tableview where the user is able to select multiple rows (these rows are distinguished by a checkbox in the row). For some reason, however, I can't implement the functionality to deselect any selected row. Can somebody tell me what I'm missing?
SomeViewController.m
#objc class SomeViewController: UIViewController, NSFetchedResultsControllerDelegate, UITableViewDataSource, UITableViewDelegate {
var deviceArray:[Device] = []
// [perform a fetch]
// [insert fetch results into deviceArray to be displayed in the tableview]
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "customcell", for:
indexPath)
// Set up the cell
let device = self.deviceArray[indexPath.row]
cell.textLabel?.text = device.name
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
self.tableView(tableView, cellForRowAt: indexPath).accessoryType = UITableViewCellAccessoryType.checkmark
NSLog("Selected Row")
}
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
self.tableView(tableView, cellForRowAt: indexPath).accessoryType = UITableViewCellAccessoryType.none
NSLog("Deselected Row")
}
}
More info:
Looking at the debug logs inserted, I can share the following observations:
When selecting an unselected row, the console prints "Selected Row"
If I click the same row from observation #1, the console prints "Selected Row" only
If I click on any other row, the console prints "Deselected Row" and then "Selected Row"
If I click on the same row as observation #3, the console prints "Selected Row" only.
So, it looks like everytime I click on a different row, tableView: didDeselectRowAt: gets called; however, checkmarks in the clicked row do not go away.
More Info 2:
So I'm new to storyboards and didn't set the "allowsMultipleSelection" property. Going into the Attributes Inspector, this is what my settings look like:
Now, when pressing the same row in the tableView, my console confirms that the app is alternating between tableView:didSelectRowAt: and tableView:didDeselectRowAt:, however, the checkmark doesn't disappear; once the user selects a row, the checkmark remains selected even when tableView:didDeselectRowAt: is called. What else am I missing?
First off make sure your datasource AND delegate outlets are set if you are setting them from storyboard.
Another thing is you need to set allowsMultipleSelection property to true to get the didSelect, didDeselect methods to get called in the behavior you want. Otherwise it will always call didSelect for the cell you tapped on and didDeselect for the most previously selected cell.
The other thing to note is that you are referencing self.tableView when setting your cell.accessoryType property. This may be different instance of the tableView being passed into the delegate method. I recommend a guard let statment to ensure the code setting the accessory type only applies if the tableView being passed into the function. Here is code I used to get it to work.
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
//Notice I use tableView being passed into func instead of self.tableView
guard let cell = tableView.cellForRow(at: indexPath) else {
return
}
cell.accessoryType = .checkmark
}
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
guard let cell = tableView.cellForRow(at: indexPath) else {
return
}
cell.accessoryType = .none
}
If you want user to be able to select multiple cells, you need to set tableView.allowsMultipleSelection = true in the viewDidLoad method or set multiple selection in the attributes inspector.