Swift 4 Table View Getting Wrong Cell - ios

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.

Related

iOS: can VoiceOver swiping work with custom UITableViewCells?

I'm having trouble with swiping between accessibility elements in a UITableView with VoiceOver on. When in a UITableView, the next/previous element in the table is focused when the user swipes right/left. The element is usually a UITableViewCell.
But I have custom cells that use subviews of the cell as accessibility elements, as in:
cell.isAccessibilityElement = false
cell.accessibilityElements = [element1, element2];
This works perfectly fine within the cell, but there's a problem swiping between cells. If the custom cell is at the bottom of the table view, it fails to focus on the next cell in the table view. Normally, using the cells themselves as accessibility elements, the table view will auto-scroll and focus on the next cell. But after the custom cell, it doesn't auto-scroll, as if there are no more cells in the table view.
See screenshot for clarification. When Label 9 is focused, the user can't swipe right to the next cells, even though there are more cells. They can still scroll though using 3 fingers, and then they'll be able to swipe to the next cells as normal.
This problem happens when using the code below. It uses a storyboard that only has a default UITableView with a default prototype cell with some UILabels added to it. So all the accessibility stuff is done here in the code.
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
let customRow = 5
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
10
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if indexPath.row == self.customRow {
return UITableView.automaticDimension
} else {
return 120
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: UITableViewCell
if indexPath.row == self.customRow {
cell = self.tableView.dequeueReusableCell(withIdentifier: "customCell", for: indexPath)
cell.isAccessibilityElement = false
cell.accessibilityElements = cell.contentView.subviews
} else {
cell = self.tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
var content = cell.defaultContentConfiguration()
content.text = "row \(indexPath.row)"
cell.contentConfiguration = content
}
return cell
}
}

TableViewCell has grey background after movement out of visible tableView

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!

UITableview reloadRows not working properly in Swift 3

I have created a UITableView with multiple sections consisting two cells with dynamic cell height. Added a button on second cell. On click of the button increasing the second cell height by changing the constraint height. Action method connected to the button is :
func increaseHeightOnClickOfButton(sender: UIButton) {
let indexPath = IndexPath(item: 1, section: sender.tag)
let cell = myTableView.cellForRow(at: indexPath) as! customCell
cell.viewHeightConstraint.constant = 100
self.myTableView.reloadRows(at: [indexPath], with: .automatic)
}
Issue: On clicking the button, cell height of second section increases rather than the first one.
I have also tried giving fix value of section like this :
let indexPath = IndexPath(item: 1, section: 0)
but it's not working.
It works only when I try this code:
func increaseHeightOnClickOfButton(sender: UIButton) {
let indexPath = IndexPath(item: 1, section: sender.tag)
let cell = myTableView.cellForRow(at: indexPath) as! customCell
cell.viewHeightConstraint.constant = 100
self.myTableView.reloadData()
}
But I don't want to reload the whole table view. Can anyone help me what I am doing wrong or is there any other approach to achieve it ?
Edit :
I did one more change, I removed :
self.myTableView.reloadRows(at: [indexPath], with: .automatic)
After clicking the button when I start scrolling the tableview, coming back to the same cell increases it's height. So I guess there is some issue with reloadRows.
You can use the following code to reload cell's height without reloading the whole data:
tableView.beginUpdates()
tableView.endUpdates()
Edit:
Modified first post to include a DataSource to track each row's height constraint constant.
Assuming your cell looks something like this:
you have added a UIView to the cell (orange view in my example)
added a label and button to the UIView
set leading, trailing, top and bottom constraints for the view
added a Height constraint to the view
connected IBOutlets and an IBAction for the button tap inside the cell class
set your table class for UITableViewAutomaticDimension
Then this should be a working example. Each time you tap a button, the Height constraint in that cell will be increased by 20-pts. No need to call reload anything...
class TypicalCell: UITableViewCell {
#IBOutlet weak var theLabel: UILabel!
#IBOutlet weak var viewHeightConstraint: NSLayoutConstraint!
var btnAction: (() -> ())?
#IBAction func didTap(_ sender: Any) {
btnAction?()
}
}
class TypicalTableViewController: UITableViewController {
// 2-D array of Row Heights
// 4 sections, with 4, 2, 6 and 3 rows
// all initialized to 40.0
var rowHeights: [[CGFloat]] = [
[40.0, 40.0, 40.0, 40.0],
[40.0, 40.0],
[40.0, 40.0, 40.0, 40.0, 40.0, 40.0],
[40.0, 40.0, 40.0]
]
override func viewDidLoad() {
super.viewDidLoad()
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 40.0
}
override func numberOfSections(in tableView: UITableView) -> Int {
return rowHeights.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return rowHeights[section].count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Typical", for: indexPath) as! TypicalCell
// Configure the cell...
cell.theLabel.text = "\(indexPath)"
// cells are reused, so set the cell's Height constraint every time
cell.viewHeightConstraint.constant = rowHeights[indexPath.section][indexPath.row]
// "call back" closure
cell.btnAction = {
// increment the value in our DataSource
self.rowHeights[indexPath.section][indexPath.row] += 20
// tell tableView it's being updated
tableView.beginUpdates()
// set the cell's Height constraint to the incremented value
cell.viewHeightConstraint.constant = self.rowHeights[indexPath.section][indexPath.row]
// tell tableView we're done updating - this will trigger an auto-layout pass
tableView.endUpdates()
}
return cell
}
}
Try reloading the Section:
self.tableView.reloadSections(IndexSet(integer: sender.tag), with: .automatic)

Swift What is the best way to replace content in TableViewCell

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.

Selecting a cell and changing the alpha of all cells in tableView - Swift

I'm working on a project that needs something I never imagined to have. The app for iOS is directed to iPad due to size. To that question, I made a small prototype to show one of the parties to detail better.
This is a tableView where the functions and actions will happen.
And this is the Swift code:
import UIKit
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
let data:[String] = ["Row 0","Row 1", "Row 2","Row 3","Row 4","Row 5","Row 6"]
#IBOutlet var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:Cell = self.tableView.dequeueReusableCellWithIdentifier("cell") as! Cell
cell.labelText.text = self.data[indexPath.row]
return cell
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let cell = self.tableView.cellForRowAtIndexPath(indexPath)
cell!.alpha = 0.5
}
}
What the app to do exactly?
Well, when a row is selected, the rows below it need to stay with the Alpha equal to 0.5.
Examples:
I touched the row 3
Action:
Row 1, Row 2 and Row 3 will keep the Alpha equal to 1.0
Row 4, Row 5 and Row 6 will keep the Alpha equal to 0.5
I touched in row 4
Action:
Row 1, Row 2, Row 3 and Row 4 will keep the Alpha equal to 1.0
Row 5, Row 6 will keep the Alpha equal to 0.5
.
.
.
Can someone help me?
You'd want to set the alpha value in the cellForRowAtIndexPath, then simply reload that row when its tapped. This should preserve the alpha for that cell and set alpha to 1 on every other cell, even if the the user scrolls the cell offscreen.
var selectedIndexPath:NSIndexPath?
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:UITableViewCell = self.tableView.dequeueReusableCellWithIdentifier("cell") as! UITableViewCell
if let selectedIndexPath = self.selectedIndexPath where indexPath.row == selectedIndexPath.row {
cell.alpha = 0.5
} else {
cell.alpha = 1
}
cell.labelText.text = self.data[indexPath.row]
return cell
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
self.selectedIndexPath = indexPath
tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
}
Make an integer variable "selectedCell", and in didSelectRowAtIndexPath, set it equal to indexPath.row, then tableView.reloadData()
In cellForRowAtIndexPath simply use an if statement to determine if indexPath.row is greater than "selectedCell". If so then set the alpha value to 0.5, otherwise set it to 1.0.
It is generally a good practice to change cell properties in cellForRowAtIndexPath, because every time it is called you risk overwriting changes you make to cells elsewhere in the code. Hope this helped.
Hi I am proposing below solution, please consider
import UIKit
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
let data:[String] = ["Row 0","Row 1", "Row 2","Row 3","Row 4","Row 5","Row 6"]
var rowSelected:Int
#IBOutlet var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:Cell = self.tableView.dequeueReusableCellWithIdentifier("cell") as! Cell
cell.labelText.text = self.data[indexPath.row]
if rowSelected <= indexPath.row
cell!.alpha = 0.5;
else
cell!.alpha = 1.0;
return cell
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let cell = self.tableView.cellForRowAtIndexPath(indexPath)
rowSelected = indexPath.row
tableView.reloadData()
}
}
I'm not sure if you are describing what you want it to do or the anormal behaviour.
There are two possibilities:
1) If you want the selection related alpha to render on a single row, you may want to override didDeselectRowAtIndexPath and set the alpha to 1.0 there.
2) You need to explicitly set the alpha after getting the cell from dequeueReusableCellWithIdentifier because you are not guaranteed to get a fresh cell instance every time.
The tableview will call cellForRowAtIndexPath for a variety of reasons even after showing it for the first time. What is probably happening is that cellForRowAtIndexPath is called for rows that have been deselected and dequeueReusableCellWithIdentifier may or may not reuse an existing cell object. When it reuses a cell object, properties are not reset.

Resources