Table View Cell getting deselected after first tap. - ios

Gif depicting that :
When user selecting table view cell for first time(checkbox ticked first time) , cell getting selected but after that it is deselected automatically and nothing happens when i am tapping second time.
But when i am tapping third time cell getting selected properly and on 4th tap it is deselecting properly and so on for 5th , 6th time onwards.
My didSelectRowAt() method looks like this :
func expandableTableView(_ expandableTableView: LUExpandableTableView, didSelectRowAt indexPath: IndexPath) {
let cell = expandableTableView.cellForRow(at: indexPath) as! FilterTableCell
let dictKey : String = FilterKeysMapping[FilterKeysFront.object(at: indexPath.section) as! String]!
if(self.FilterDictAPI[dictKey] == nil){
self.FilterDictAPI[dictKey] = [indexPath.row: self.FilterValueArray.object(at: indexPath.row)]
}
else{
self.FilterDictAPI[dictKey]![indexPath.row] = self.FilterValueArray.object(at: indexPath.row)
}
self.expandableTableView.beginUpdates()
cell.button.isSelected = true
self.expandableTableView.reloadRows(at: [indexPath], with: .automatic)
self.expandableTableView.endUpdates()
expandableTableView.selectRow(at: indexPath, animated: true, scrollPosition: .none)
}
didDeselectRowAt() method is like this :
func expandableTableView(_ expandableTableView: LUExpandableTableView, didDeselectRowAt indexPath: IndexPath) {
print("Did Deselect Cell at section \(indexPath.section) row \(indexPath.row)")
let cell = expandableTableView.cellForRow(at: indexPath) as! FilterTableCell
cell.button.isSelected = false
let dictKey : String = FilterKeysMapping[FilterKeysFront.object(at: indexPath.section) as! String]!
if(self.FilterDictAPI[dictKey] != nil){
self.FilterDictAPI[dictKey]?.removeValue(forKey: indexPath.row)
}
print("dict after removing values : \(self.FilterDictAPI)")
}
cellForRowAt() method is :
func expandableTableView(_ expandableTableView: LUExpandableTableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = expandableTableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier) as? FilterTableCell else {
assertionFailure("Cell shouldn't be nil")
return UITableViewCell()
}
cell.selectionStyle = UITableViewCellSelectionStyle.none
cell.label.text = "\(self.FilterValueArray.object(at: indexPath.row))" + " (" + "\(self.FilterCountArray.object(at: indexPath.row))" + ")"
return cell
}
Table View Cell is :
class FilterTableCell: UITableViewCell {
let label = UILabel()
let button = UIButton()
var check = Bool()
// MARK: - Init
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(label)
contentView.addSubview(button)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Base Class Overrides
override func layoutSubviews() {
super.layoutSubviews()
label.frame = CGRect(x: 42, y: 0, width: contentView.frame.width-42, height: contentView.frame.height)
self.label.font = UIFont(name: "PlayfairDisplay-Regular", size: 18)
button.frame = CGRect(x:10, y: contentView.frame.height/2-8, width: 16, height: 16)
button.setImage(UIImage(named: "CheckboxUnchecked"), for: .normal)
button.setImage(UIImage(named: "CheckboxChecked"), for: .selected)
button.setImage(UIImage(named: "CheckboxUnchecked"), for: .highlighted)
}
}
Issue as mentioned is only this : After first tap it is deselecting automatically.

What's exactly happening is in didSelectRowAt when you reload that indexPath, that cell is deselected automatically and didDeselectRowAt method is called where cell.button.isSelected = false removes the checkmark.
So, to fix this comment out the below lines in didSelectRowAt method.
self.expandableTableView.beginUpdates()
self.expandableTableView.reloadRows(at: [indexPath], with: .automatic)
self.expandableTableView.endUpdates()
Also, reset button's selected state in cell's prepareForReuse() method. This will fix the undefined behaviour where checkbox is selected randomly or after first or second taps.
override func prepareForReuse() {
super.prepareForReuse()
button.isSelected = false
}

Related

UISwitch is not invoking the function when added with tableView cell

I have added a switch along with each cell in table view but the switch function is not get called. If I give the switch in the front page its displaying successfully. But in tableview cell its not working `
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = models[indexPath.row].Address
cell.textLabel?.text = models[indexPath.row].Number
cell.textLabel?.text = models[indexPath.row].Role
cell.textLabel?.text = models[indexPath.row].Name
//switch
let mySwitch = UISwitch(frame: .zero)
mySwitch.setOn(false, animated: true)
mySwitch.tag = indexPath.row
mySwitch.tintColor = UIColor.red
mySwitch.onTintColor = UIColor.green
mySwitch.addTarget(self, action: #selector(switchValueDidChange(_:)), for: .valueChanged)
cell.accessoryView = mySwitch
return cell
}
#IBAction func switchValueDidChange(_sender: UISwitch){
if _sender .isOn{
print("switch on")
view.backgroundColor = UIColor.red }
else{
view.backgroundColor = UIColor.systemPurple
}
}
`
The signature is wrong. There must be a space character between the underscore and sender. And if it's not a real IBAction replace #IBAction with #objc
#objc func switchValueDidChange(_ sender: UISwitch) {
if sender.isOn {...
and – not related to the issue – the selector can be simply written
#selector(switchValueDidChange)

UIControl not recognizing tap inside table view cell, stopped working from iOS 14

I have an application I started, before iOS 14 came out. I stopped with Swift for few months, and now, the emulator is running iOS 14, since then, I have a problem that a UIControl I have inside a tableView cell does not register taps.
This is my TableView (plain, nothing custom or anything):
private let tableView: UITableView = {
let table = UITableView(frame: .zero, style: .plain)
table.translatesAutoresizingMaskIntoConstraints = false
table.backgroundColor = .white
table.separatorInset.right = table.separatorInset.left
return table
}()
This is how I try to set the tap gesture:
extension TodoListVC: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: todoCellIdentifier, for: indexPath) as! TodoCell
let todo = todoListViewModel.todos[indexPath.row]
cell.model = todo
cell.selectionStyle = .none
let checkBox = cell.checkbox
checkBox.index = indexPath.row
checkBox.addTarget(self, action: #selector(onCheckBoxValueChange(_:)), for: .valueChanged) // touchUpInside also does not work.
return cell
}
#objc func onCheckBoxValueChange(_ sender: UICheckBox) {
var todo = todoListViewModel.todos[sender.index]
todo.isDone = sender.isChecked
tableView?.reloadRows(at: [IndexPath(row: sender.index, section: 0)], with: .none)
todoListView.reloadProgress()
}
}
The checkbox is a custom UIControl I copied from a tutorial, it worked great before iOS 14, and I can't figure out what's the problem with it now. The file is quite large to add here, but if this is necessary I can provide the code.
Go to your TodoCell and add this in your initializer:
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.isUserInteractionEnabled = true
}
It will start working again. If you have not created the cells programatically then you can do this:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: todoCellIdentifier, for: indexPath) as! TodoCell
let todo = todoListViewModel.todos[indexPath.row]
cell.model = todo
cell.selectionStyle = .none
cell.contentView.isUserInteractionEnabled = true
let checkBox = cell.checkbox
checkBox.index = indexPath.row
checkBox.addTarget(self, action: #selector(onCheckBoxValueChange(_:)), for: .valueChanged) // touchUpInside also does not work.
return cell
}
To debug, you can print the value of contentView.isUserInteractionEnabled and it will give you the boolean and using that you can see if it is the issue or not. Most probably it will return false and the above solution works well for iOS 14 and Xcode 12.

Scrolling tableview causes checkmark to disappear | addressing reusable cells in Swift 5

I use the following to mark rows in a tableview as either marked with a checkmark or unselected with no checkmark. The issue that I have stumbled on is when scrolling the tableView seems to reload and cause the checkmark to disappear.
I understand this is caused by reusable cells,
Is there an easy fix I can implement into the code below?
class CheckableTableViewCell: UITableViewCell {
var handler: ((Bool)->())?
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.selectionStyle = .none
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
self.accessoryType = selected ? .checkmark : .none
handler?(selected)
}
}
class TableViewController: UITableViewController {
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! CheckableTableViewCell
cell.handler = {[weak self](selected) in
selected ? self?.selectRow(indexPath) : self?.unselectRow(indexPath)
}
let section = sections[indexPath.section]
let item = section.items[indexPath.row]
cell.textLabel?.textAlignment = .left
cell.selectionStyle = .none
let stringText = "\(item.code)"
cell.textLabel!.text = stringText
return cell
}
UPDATE:
struct Section {
let name : String
var items : [Portfolios]
}
struct Portfolios: Decodable {
let code: String
var isSelected: Bool
enum CodingKeys : String, CodingKey {
case code
}
}
You need to create your data model having a property called isSelected: Bool then use this to decide when a row should be selected or not. Note that you have to toggle this property every time didSelectRowAt(indexPath:) is triggered.
Example
// Declare your model with one of the property called isSelected
class MyModel {
var isSelected: Bool
init(isSelected: Bool) {
self.isSelected = isSelected
}
}
class TableViewController: UITableViewController {
// Dummy data note that it contains array of "MyModel" which have the "isSelected" property.
private var myModels: [MyModel] = [MyModel(isSelected: false),MyModel(isSelected: true),MyModel(isSelected: true),MyModel(isSelected: false) ]
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! CheckableTableViewCell
cell.handler = {[weak self](selected) in
/// Note that I am using selected Property here
myModels[indexPath.row].isSelected ? self?.selectRow(indexPath) : self?.unselectRow(indexPath)
}
let section = sections[indexPath.section]
let item = section.items[indexPath.row]
cell.textLabel?.textAlignment = .left
cell.selectionStyle = .none
let stringText = "\(item.code)"
cell.textLabel!.text = stringText
return cell
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let selectedIndexPath = indexPath
myModels[selectedIndexPath.row].isSelected = true
self.tableView.reloadRows(at: [selectedIndexPath], with: .none)
}
}

Strange behavior with cell construction with Simple TableView Swift

I have spent an insane time to fix this bug in our application. We are currently working in a chat and we have used a tableview for it.
Our tableview works fine until there is a certain amount of messages, after that, the table starts to flick. I coded without storyboards and for this reason I thought that constraints were the cause of trouble. So I decide to make a really simple tableview with some features of our chat tableview (actually our tableview is coredata linked and with a lot of graphic stuffs).
Because I suspected about constraints, I didn't use it in the code bellow just to see everything works fine, but it wasn't the case. In the gif image we can see two undesirable behaviors, the first one is that sometimes the table is re-generated completely, so cells disappear and appear in a very short time (this cause a very annoying flick). The second is no less annoying: cells are duplicated (I think this is for the cell reusability feature) but after a short period of time they are accommodated and everything goes fine.
https://github.com/hugounavez/pizzaWatch/blob/master/videoBug.gif
I tried adding the prepareForReuse() method and delete the views and creates them in the cell again but no results.
This is the example code, you can copy and run it with no problems in a Playground:
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
class Modelito{
// This is the class tableview model
var celda: String
var valor: String
init(celda: String, valor: String){
self.celda = celda
self.valor = valor
}
}
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource{
let tableview: UITableView = {
let table = UITableView()
table.translatesAutoresizingMaskIntoConstraints = false
return table
}()
let button: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Click me to add new cell", for: .normal)
button.setTitle("Click me to add new cell", for: .highlighted)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
var model: [Modelito] = []
let tipoDeCelda = ["MyCustomCell", "MyCustomCell2"]
override func viewDidLoad() {
super.viewDidLoad()
self.setupViews()
self.tableview.register(MyCustomCell.self, forCellReuseIdentifier: "MyCustomCell")
self.tableview.register(MyCustomCell2.self, forCellReuseIdentifier: "MyCustomCell2")
self.tableview.dataSource = self
self.tableview.delegate = self
self.button.addTarget(self, action: #selector(self.addRow), for: .touchUpInside)
// Here I generate semi random info
self.dataGeneration()
}
func setupViews(){
self.view.addSubview(self.tableview)
self.tableview.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
self.tableview.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -50).isActive = true
self.tableview.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: 1).isActive = true
self.tableview.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
self.tableview.backgroundColor = .gray
self.view.addSubview(self.button)
self.button.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
self.button.topAnchor.constraint(equalTo: self.tableview.bottomAnchor).isActive = true
self.button.leftAnchor.constraint(equalTo: self.view.leftAnchor).isActive = true
self.button.rightAnchor.constraint(equalTo: self.view.rightAnchor).isActive = true
self.button.backgroundColor = .orange
}
func dataGeneration(){
let number = 200
// Based in the cell types availables and the senteces, we create random cell info
for _ in 0...number{
self.model.append(
Modelito(celda: tipoDeCelda[Int(arc4random_uniform(UInt32(self.tipoDeCelda.count)))], valor: "\(self.model.count)")
)
}
self.tableview.reloadData()
// After we insert elements in the model we scroll table
let indexPaths: [IndexPath] = [IndexPath(row: self.model.count - 1, section: 0)]
self.tableview.scrollToRow(at: indexPaths[0], at: .bottom, animated: false)
}
#objc func addRow(){
// This function insert a new random element
self.tableview.beginUpdates()
self.model.append(
Modelito(celda: tipoDeCelda[Int(arc4random_uniform(UInt32(self.tipoDeCelda.count)))], valor: "\(self.model.count)")
)
// After inserting the element in the model, we insert it in the tableview
let indexPaths: [IndexPath] = [IndexPath(row: self.model.count - 1, section: 0)]
self.tableview.insertRows(at: indexPaths, with: .none)
self.tableview.endUpdates()
// Finally we scroll to last row
self.tableview.scrollToRow(at: indexPaths[0], at: .bottom, animated: false)
}
}
extension ViewController{
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.model.count
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 150
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let celldata = self.model[indexPath.row]
switch celldata.celda {
case "MyCustomCell":
let cell = tableview.dequeueReusableCell(withIdentifier: "MyCustomCell", for: indexPath) as! MyCustomCell
cell.myLabel.text = self.model[indexPath.row].valor
return cell
default:
let cell = tableview.dequeueReusableCell(withIdentifier: "MyCustomCell2", for: indexPath) as! MyCustomCell2
cell.myLabel.text = self.model[indexPath.row].valor
return cell
}
}
}
class MyCustomCell: UITableViewCell {
var myLabel = UILabel()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
myLabel.backgroundColor = UIColor.green
self.contentView.addSubview(myLabel)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func layoutSubviews() {
super.layoutSubviews()
myLabel.frame = CGRect(x: 25, y: 0, width: 370, height: 30)
}
}
class MyCustomCell2: UITableViewCell {
var myLabel = UILabel()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
myLabel.backgroundColor = UIColor.yellow
self.contentView.addSubview(myLabel)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func layoutSubviews() {
super.layoutSubviews()
myLabel.frame = CGRect(x: 0, y: 0, width: 370, height: 30)
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = ViewController()
Thanks in advance.
Edit:
I changed the code base in the #Scriptable answer in order to be compatible with playground. At this point, I am starting to think that duplication cell bug is actually normal for tableview. In order to see the problem, the button should be pressed several times fastly.
This code is reloading the tableview but it's removing that "annoying flick" and it's too smooth.
To fix that annoying flick just change addRow() function to
#objc func addRow(){
self.model.append(
Modelito(celda: tipoDeCelda[Int(arc4random_uniform(UInt32(self.tipoDeCelda.count)))], valor: "\(self.model.count)")
)
self.tableview.reloadData()
self.scrollToBottom()
}
scrollToBottom() function:
func scrollToBottom(){
DispatchQueue.global(qos: .background).async {
let indexPath = IndexPath(row: self.model.count-1, section: 0)
self.tableview.scrollToRow(at: indexPath, at: .bottom, animated: true)
}
}
Just try it.
Using your current code in a playground produced a fatal error for me.
2017-11-13 15:10:46.739 MyPlayground[4005:234029] *** Terminating app due to uncaught exception 'NSRangeException', reason: '-[UITableView _contentOffsetForScrollingToRowAtIndexPath:atScrollPosition:]: row (200) beyond bounds (0) for section (0).'
I changed the dataGeneration function to the following code which just calls reloadData once the data has been generated and scrolls to the bottom.
I think the error was that without reloading, there wasn't that many rows to scroll to.
func dataGeneration(){
let number = 200
// Based in the cell types available and the sentences, we create random cell info
for _ in 0...number{
self.model.append(
Modelito(celda: tipoDeCelda[Int(arc4random_uniform(UInt32(self.tipoDeCelda.count)))], valor: "\(self.model.count)")
)
}
self.tableview.reloadData()
// After we insert elements in the model we scroll table
let indexPaths: [IndexPath] = [IndexPath(row: self.model.count - 1, section: 0)]
self.tableview.scrollToRow(at: indexPaths[0], at: .bottom, animated: false)
}
Once I got around the crash, the tableView worked fine for me. No flickering or duplication.

Want accessory checkmark to show only when tapped on the right

I have a TableView with cells that when pressed anywhere in the cell, it adds a checkmark on the right. I only want the checkmark to show up if the cell is tapped on the right side. Here's the pertinent section of code from the TableViewController:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TaskCell", for: indexPath) as! TaskCell
let task = tasks[indexPath.row]
cell.task = task
if task.completed {
cell.accessoryType = UITableViewCellAccessoryType.checkmark;
} else {
cell.accessoryType = UITableViewCellAccessoryType.none;
}
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: false)
var tappedItem = tasks[indexPath.row] as Task
tappedItem.completed = !tappedItem.completed
tasks[indexPath.row] = tappedItem
tableView.reloadRows(at: [indexPath], with: UITableViewRowAnimation.none)
}
}
Is there a simple way to do that, or to do it using storyboard? My Swift skills leave a LOT to be desired. Any help would be appreciated! Thank you!
Instead of the built-in checkmark accessory type, why not provide, as accessory view, an actual button that the user can tap and that can display the checkmark? The button might, for example, display as an empty circle normally and as a circle with a checkmark in it when the user taps it.
Otherwise, you're expecting the user to guess at an obscure interface, whereas, this way, it's perfectly obvious that you tap here to mark the task as done.
Example:
To accomplish that, I created a button subclass and set the accessoryView of each cell to an instance of it:
class CheckButton : UIButton {
convenience init() {
self.init(frame:CGRect.init(x: 0, y: 0, width: 20, height: 20))
self.layer.borderWidth = 2
self.layer.cornerRadius = 10
self.titleLabel?.font = UIFont(name:"Georgia", size:10)
self.setTitleColor(.black, for: .normal)
self.check(false)
}
func check(_ yn:Bool) {
self.setTitle(yn ? "✔" : "", for: .normal)
}
override init(frame:CGRect) {
super.init(frame:frame)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
The title of the button can be the empty string or a checkmark character, thus giving the effect you see when the button is tapped. This code comes from cellForRowAt::
if cell.accessoryView == nil {
let cb = CheckButton()
cb.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
cell.accessoryView = cb
}
let cb = cell.accessoryView as! CheckButton
cb.check(self.rowChecked[indexPath.row])
(where rowChecked is an array of Bool).
You will have to define your own accessory button, and handle its own clicks.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TaskCell", for: indexPath) as! TaskCell
let task = tasks[indexPath.row]
cell.task = task
let checkButton = UIButtonSubclass()
...configure button with your circle and check images and a 'selected property'...
checkButton.addTarget(self, action:#selector(buttonTapped(_:forEvent:)), for: .touchUpInside)
cell.accessoryView = checkButton
checkButton.selected = task.completed //... this should toggle its state...
return cell
}
func buttonTapped(_ target:UIButton, forEvent event: UIEvent) {
guard let touch = event.allTouches?.first else { return }
let point = touch.location(in: self.tableview)
let indexPath = self.tableview.indexPathForRow(at: point)
if let task = tasks[indexPath.row] {
task.completed = !task.completed
}
tableView.reloadData() //could also just reload the row you tapped
}
Though, it has been noted that using tags to detect which row was tapped is dangerous if you start to delete rows. You can read more here https://stackoverflow.com/a/9274863/1189470
EDITTED
Removed the reference to tags per #matt

Resources