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

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.

Related

How to handle multiple buttons in a custom UITableViewCell?

I have a custom UITableViewCell which has 2 buttons (for incrementing and decrementing) and a count label in it. What I want to achieve is to update countLabel appropriately when subtractButton or addButton is tapped.
My custom cell class:
class ItemOptionCell: UITableViewCell {
private var count = 0
private var countLabel: UILabel = {
let label = UILabel()
label.textColor = .black
label.font = UIFont.systemFont(ofSize: 14)
label.numberOfLines = 0
label.text = "0"
label.adjustsFontSizeToFitWidth = true
return label
}()
private let subtractButton: UIButton = {
let subButton = UIButton(type: .system)
subButton.setTitle("-", for: .normal)
subButton.addTarget(self, action: #selector(decreaseItemCount), for: .touchUpInside)
return subButton
}()
private let addButton: UIButton = {
let addButton = UIButton(type: .system)
addButton.setTitle("+", for: .normal)
addButton.addTarget(self, action: #selector(increaseItemCount), for: .touchUpInside)
return addButton
}()
// contains subtract, add buttons and item count
private var operationsStackView = UIStackView()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
backgroundColor = .white
configureOperationsStackView()
}
func set(itemOption: ItemOption) {
itemLabel.text = itemOption.title
}
private func configureOperationsStackView() {
addSubview(operationsStackView)
// code for autolayout
}
#objc private func decreaseItemCount() {
if count > 0 {
count -= 1
}
updateCountLabel()
}
#objc private func increaseItemCount() {
count += 1
updateCountLabel()
}
private func updateCountLabel() {
countLabel.text = String(count)
}
Part of ViewController for handling table view delegates:
extension ItemOptionsViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let label = UILabel()
label.text = "Header #1"
label.backgroundColor = .orange
return label
}
func numberOfSections(in tableView: UITableView) -> Int {
return options.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return options[section].count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellId) as! ItemOptionCell
let option = options[indexPath.section][indexPath.row]
cell.set(itemOption: ItemOption(title: option))
cell.selectionStyle = .none
return cell
}
}
All the forums I looked up only describe how to handle single buttons.
P.s. I read about assigning tags to buttons but found out that it's not a recommended way as when row count changes managing tags becomes problematic. Therefore, if possible, recommend a way with delegates or closures.
Use delegate for this purpose
protocol ItemOptionCellDelegate: AnyObject {
func didDecreaseItemTapped(in cell: ItemOptionCell)
func didIncreaseItemCount(in cell: ItemOptionCell)
}
class ItemOptionCell: UITableViewCell {
weak var delagate: ItemOptionCellDelegate?
...
private func decreaseItemCount() {
delegate?.didDecreaseItemTapped(in: self)
}
private func increaseItemCount() {
delegate?.didIncreaseItemCount(in: self)
}
in your ViewController
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellId) as! ItemOptionCell
let option = options[indexPath.section][indexPath.row]
cell.set(itemOption: ItemOption(title: option))
cell.selectionStyle = .none
cell.delegate = self
cell.countLabel.text = //some value
return cell
}
extension ItemOptionsViewController: ItemOptionCellDelegate {
func didDecreaseItemTapped(in cell: ItemOptionCell) {
guard let indexPath = tableView.indexPath(for: cell) else { return }
let option = options[indexPath.section][indexPath.row]
//do some stuff with your data, then reload table and set need value for count label.
}
func didIncreaseItemCount(in cell: ItemOptionCell) { ... }
Also, DON'T update countLabel inside cell implementation, basically you have to set value count from your model, for example in ViewController in func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell func (in case you use MVC approach)
Forget about tags.
Create gesture recognisers in your cellForRowAt and assign them to the buttons:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
...
//Create gesture
//Create two of these: one will be for the add button and one for the minus one. Attach to eachh gesture a different function.
let tap = UITapGestureRecognizer(target: self, action: #selector(tapped))
//Add the two different gestures to both of the buttons
cell.myButton.addGestureRecognizer(tap)
...
}
//Create two of these: one to add and one to subtract
#objc func tapped(){
print("ok")
//Perform your action here
}
Plus, when it comes to adding the buttons in the tableViewCell custom class, call contentView.addSubview(myButton) instead of addSubview(myButton).
Oh and obviously, remove all the stuff in your custom class where you add targets to buttons and stuff like that, only create and add the objects to the cell as I said before.
Edit:
Set your buttons as follows:
//Do this for both buttons
private let subtractButton: UIButton = {
let subButton = UIButton(type: .system)
subButton.setTitle("-", for: .normal)
return subButton
}()

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)
}
}

How to avoid adding more than one tap gesture to any cell?

I have a weird problem. When scrolling down, cells disappear if Tap Gesture happened.
Looks like I need to stop adding Tap Gesture to cells. I've done testing of this condition in function but it didn't work.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! ToDoItemsCell
...
cell.textField.delegate = self
cell.textField.isHidden = true
cell.toDoItemLabel.isUserInteractionEnabled = true
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(toDoItemLabelTapped))
tapGesture.numberOfTapsRequired = 1
cell.addGestureRecognizer(tapGesture)
return cell
}
And here is my function:
#objc func toDoItemLabelTapped(_ gesture: UITapGestureRecognizer) {
if gesture.state == .ended {
let location = gesture.location(in: self.tableView)
if let indexPath = tableView.indexPathForRow(at: location) {
if let cell = self.tableView.cellForRow(at: indexPath) as? ToDoItemsCell {
cell.toDoItemLabel.isHidden = true
cell.textField.isHidden = false
cell.textField.becomeFirstResponder()
cell.textField.text = cell.toDoItemLabel.text
}
}
}
}
Tapping works, but it keeps adding to other cells and makes them disappear. What can be the issue?
Gesture should be added once to each cell. In your code gesture will be added every time cellForRowAt will be called and it will be called many times especially when you scroll down to list.
Move you gesture add code to ToDoItemsCell class and than you can use delegates to inform your view controller when cell gets tapped.
protocol ToDoItemsCellDelegate {
toDoItemsCellDidTapped(_ cell: ToDoItemsCell)
}
class ToDoItemsCell : UITableViewCell {
weak var delegate: ToDoItemsCellDelegate?
var indexPath: IndexPath!
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
// code common to all your cells goes here
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(toDoItemLabelTapped))
tapGesture.numberOfTapsRequired = 1
self.addGestureRecognizer(tapGesture)
}
#objc func toDoItemLabelTapped(_ gesture: UITapGestureRecognizer) {
delegate?.toDoItemsCellDidTapped(self)
}
}
In function cellForRowAt you can just select the delegate and set indexPath.
Note:
If you just wanted to perform action when user taps any cell you can use didSelectRowAt method of UITableViewDelegate.

Table View Cell getting deselected after first tap.

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
}

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