UIContextMenu not appearing when UITableViewCell also contains a UIButton - swift

I am creating a simple to do list app and the UITableViewCell has a checkmark button in it along with a title. When clicking the checkmark button, the done status changes and the cell UI updates.
I also would like to utilise the long press gesture within the UITableViewCell to have a ContextMenu appear. Everything seems to be working, however when a user long presses over the checkmark button, the ContextMenu doesn't appear.
I assume there are competing gestures (ie the UIButton and the cell's Long Press), but I'm having trouble figuring out how to support both at the same time. I've created the test code below to simplify the issue. In storyboard the cell has a single button on it. When long pressed, the context menu does not appear (as I need it to).
I know there's a solution somewhere!
class TableViewController: UITableViewController {
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return tableView.dequeueReusableCell(withIdentifier: "cellTest", for: indexPath)
}
override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
let actions = [UIAction(title: "Delete", image: UIImage(systemName: "trash"), attributes: .destructive, handler: { action in
return
})]
return UIContextMenuConfiguration(actionProvider: { _ in
return UIMenu(options: .displayInline, children: actions)
})
}
}

Related

UITableViewCell missing deletion control after successive left-swipes

I use a tableview with editable custom style cells. There's an Edit button in the navigation bar which toggles editing mode. I need the editing mode to delete and reorder rows.
When I left-swipe a cell, the Delete action button shows on the right side of that cell. While it is displayed, I press the Edit button and the Delete action button disappears. Pressing the Edit button again shows the deletion command left and the reordering controls right of ALL(!) cells. So far all as expected.
Now (as an example), when I left-swipe cell 0, then left-swipe cell 1 while Delete action button in cell 0 is still visible, activating editing mode with Edit button now shows deletion command left and reordering control right of all cells, EXCEPT(!) cell 0.
I found out that as soon as 2 or more cells are left-wiped, the first cell which is not right-swiped back out of editing mode, misses the deletion control (left) when editing mode for all cells is enabled using Edit button. Even more weird ... on subject cell missing deletion control (left), the reordering control (right) shows correctly!
I followed and compared many tutorials but didn't catch the error.
What do I miss?
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
...
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
...
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
...
}
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
...
}
override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
...
}
override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
...
}
// not necessary = didn't change anything, but found in many tutorials
override func setEditing(_ editing: Bool, animated: Bool) {
super.setEditing(editing, animated: animated)
}
#IBAction func editAction(_ sender: Any) { // that's the Edit button action
self.setEditing(!isEditing, animated: true)
self.navigationItem.leftBarButtonItem?.title = isEditing ? "Done" : "Edit"
}
Above all involved methods. Irrelevant method content is omitted to keep the post compact. I will add/edit if deemed required.
View after left-swipe of cell 1 (disregard Rename and Share actions).
View after left-swipe of cell 2, then cell 1, then entering editing mode using Edit button.
I found the reason for the misbehaviour. Since I use animation in the setEditing() method, I need to to tell the view that there are updates using beginUpdates() and endUpdates(). That did the job! No badly displayed cells anymore neither simulator, nor device!
#IBAction func editAction(_ sender: Any) {
tableView.beginUpdates()
self.setEditing(!isEditing, animated: true)
tableView.endUpdates()
self.navigationItem.leftBarButtonItem?.title = isEditing ? "Done" : "Edit"
}
EDIT 1
Above (beginUpdates() and endUpdates()) works as desired, but below (performBatchUpdates {}) is more modern (iOS 11.0) and should be preferred.
#IBAction func editAction(_ sender: Any) {
tableView.performBatchUpdates {
self.setEditing(!isEditing, animated: true)
}
self.navigationItem.leftBarButtonItem?.title = isEditing ? "Done" : "Edit"
}
EDIT 2
Meanwhile I believe that the OP misbehaviour is an iOS bug.
By chance, I found that the Safari Bookmarks tableview behaves the same way. Left-swipe cell 0, then left-swipe cell 1, next press Done/Edit button (lower right corner) twice. You can observe that cell 0 doesn't show the deletion control icon (one-way-sign) left side of cell 0. Consequently I believe this is a iOS bug.
Besides this, other apps produce the same misbehaviour on editing tableviews.
Hi I believe your problem can be fixed by using UITableView's preset editButtonItem.
navigationItem.leftBarButtonItem = self.editButtonItem
this sets state of the button into edit mode when a swipe action is being performed, therefore should fix the issue you are having.

Why is the leading swipe action also duplicated as a trailing action?

I have implemented a leading swipe action ('Delete') on my tableView which for a reason I can't figure out is also appearing as a trailing swipe action. See code below:
func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) ->
UISwipeActionsConfiguration? {
let delete1 = deleteAction(at: indexPath)
return UISwipeActionsConfiguration(actions: [delete1])
}
func deleteAction(at indexPath: IndexPath) -> UIContextualAction {
let action = UIContextualAction(style: .destructive, title: "Delete") { (action, view, completion) in
self.delete(at: indexPath)
}
return action
}
I used to have a trailing swipe action, but I deleted this function out completely. When I change 'leadingSwipeActionsConfigurationForRowAt' to 'trailingSwipeActions...' then only the trailing swipe action appears. Be grateful if anyone could tell me what I've missed. Thanks.
Use this code to prevent trailingSwipeAction()
func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle
{
return .none
}
or
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
return UISwipeActionsConfiguration(actions: [])
}
Because that is the default behaviour, when swipes are enabled. You can do something like this to disable swipes on the trailing side, if you want to implement the destructive delete action on the left only.
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
return UISwipeActionsConfiguration(actions: [])
}
By passing an empty set of actions, the trailing swipe will disappear due to having 0 set of possible actions.

Swift4: How to change UITableView cell color

I have a UITable view where you can add items by pressing a button. I want its cells to be clear, I already made its background semitransparent, but once you click on the button that allows you to save the items in it, the first you save has a white background. Then, if you press the button other times, all of the cells, except for the last created, become semitransparent, but one continues to be white. This is my code, how could I make it completely clear?
extension ViewController : UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return number.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let textCell = tableView.dequeueReusableCell(withIdentifier: "WordCell", for: indexPath)
textCell.textLabel?.text = "\(indexPath.row + 1) \(number[indexPath.row])"
let semitransparentBlack = UIColor(rgb: 0x000000).withAlphaComponent(0.3)
textCell.layer.backgroundColor = semitransparentBlack.cgColor
textCell.textLabel?.layer.backgroundColor = semitransparentBlack.cgColor
textCell.textLabel?.textColor = UIColor.white
return textCell
}
}
You can set the color using textCell.backgroundColor = semitransparentBlack

Swift - Making a Button for UITableViewCell with .addTarget

I'm trying to make two different buttons for each cell that I create in my table view. One of the buttons is a + button that will increment a label. In my testing however I cannot get the function to work. My current error says
Argument of #selector does not refer to an '#objc' method, property, or initializer
I feel like I'm implementing the .addTarget method completely wrong but I am new. Here is my code:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let item = items[indexPath.section]
let cell = tableView.dequeueReusableCell(withIdentifier: "itemCell") as! AddItemCell
cell.setCell(item: item)
let itemAmount = cell.itemAmount as UILabel?
cell.addButton.addTarget(self, action: #selector(addItem(sender:cell.addButton,forLabel:itemAmount!)), for: .touchUpInside)
}
#objc func addItem(sender: UIButton, forLabel label:UILabel) {
print("Add Button Clicked")
}
You are using selector syntax incorrectly:
action: #selector(addItem(sender:cell.addButton,forLabel:itemAmount!))
Just say:
action: #selector(addItem)
Then, however, you will face a new problem. You think that somehow you can cause this button to call something called addItem(sender:forLabel:). You can't. Change the declaration of addItem to addItem(_ sender:UIButton). That is the only kind of function a button tap can call.
You will thus have the sender (the button), but you must figure out from there what the label is. (And this should be easy, because, knowing the button, you know the cell, and knowing the cell, you know the label.) You cannot pass the label as a parameter in response to the button tap — but you don't need to.
You need to create callback function in you cell
class AddItemCell: UITableViewCell {
var buttonClickCallback:(() -> Void)?
#IBAction func onButtonClick(_ sender:Any) {
buttonClickCallback?()
}
}
and assign buttonClickCallback in tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell method
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let item = items[indexPath.section]
let cell = tableView.dequeueReusableCell(withIdentifier: "itemCell") as! AddItemCell
cell.setCell(item: item)
let itemAmount = cell.itemAmount as UILabel?
cell.buttonClickCallback = {
self.addItem(sender:cell.addButton,forLabel:itemAmount!)
}
}

didSelectRowAt indexPath does not when tapped (but does trigger when cell is swiped in either direction)

This is a weird (but should be simple) one. I am simply trying to tap a custom UITableViewCell and trigger a segue to go to another ViewController. didSelectRowAt indexPath does not trigger when the cell is tapped, but it oddly enough does when the cell is swiped from right to left or left to right.
My didSelectRowAt indexPath:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("You selected cell number: \(indexPath.row)!")
self.performSegue(withIdentifier: "mySegue", sender: self)
}
(as suggested here)
My ViewController:
import UIKit
class myViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UIPickerViewDelegate {
var myData = NSDictionary()
override func viewDidLoad() {
super.viewDidLoad()
fetchMyData()
self.tableView.delegate = self
self.tableView.dataSource = self
self.tableView.rowHeight = 100
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
func fetchMyData() {
// ... fetches my data
}
////////////////////////////////////////////////
//Table view data source
//set number of sections in table
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
//set number of rows in table
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return myData.count
}
//delegate information to cells in table rows
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
print("running tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell...")
// Dequeue cell
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! myCustomCell
cell.cellAmount!.text = "Hello World"
cell.cellTextField2!.text = "Some info"
cell.cellTextField3!.text = "Some other info"
cell.backgroundCardView.backgroundColor = UIColor.red
cell.backgroundCardView.layer.cornerRadius = 8.0
cell.backgroundCardView.layer.masksToBounds = false
cell.backgroundCardView.layer.shadowColor = UIColor.black.withAlphaComponent(0.9).cgColor
cell.backgroundCardView.layer.shadowOffset = CGSize(width: 0, height: 0)
cell.backgroundCardView.layer.shadowOpacity = 0.9
print("finished tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell...")
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("You selected cell number: \(indexPath.row)!")
self.performSegue(withIdentifier: "mySegue", sender: self)
}
// func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
// let delete = UITableViewRowAction(style: .destructive, title: "Delete") { (action, indexPath) in
// // delete item at indexPath
// }
// let share = UITableViewRowAction(style: .normal, title: "Disable") { (action, indexPath) in
// // share item at indexPath
// }
//
// share.backgroundColor = UIColor.blue
//
// return [delete, share]
// }
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
return []
}
// Reload the table data when a change is made
// func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
// self.tableView.reloadData()
// }
/*
// Override to support conditional editing of the table view.
override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
// Return false if you do not want the specified item to be editable.
return true
}
*/
/*
// Override to support rearranging the table view.
override func tableView(tableView: UITableView, moveRowAtIndexPath fromIndexPath: NSIndexPath, toIndexPath: NSIndexPath) {
}
*/
/*
// Override to support conditional rearranging of the table view.
override func tableView(tableView: UITableView, canMoveRowAtIndexPath indexPath: NSIndexPath) -> Bool {
// Return false if you do not want the item to be re-orderable.
return true
}
*/
//Table view data source (end)
////////////////////////////////////////////////
}
I have already tried the following as well:
-didSelectRowAtIndexPath: not being called
How to detect Cell selection in UITableView - Swift
Push segue from UITableViewCell to ViewController in Swift
How to tap cell's buttons in UITableViewCell without actionning the cell's segue
is it possible to segue from a UITableViewCell on a UIView to another view
UITableView didSelectRowAt is not called iOS 10, but works for 9.3.5
The UITableViewCell does have UIView and multiple UITextFields, so I did make sure that User Interaction Enabled is unchecked for the UIView and UITextFields.
EDIT: I have also looked in the debugger navigator and can see that the CPU usage percentage increases when I tap a cell (). Maybe there is a way to step deeper and see why that tap doesn't then cause the didSelectRowAt indexPath to be triggered..?
Does anyone know what could possibly be causing didSelectRowAt indexPath to not be triggered when the cell is tapped?
I faced the same issue and found an answer that was applicable for me here. I had this code for keyboard dismiss in viewWillAppear:
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self.view, action: #selector(UIView.endEditing(_:))))
After commenting this line tapping started to work! But now I will have to find better way for keyboard dismiss. :) Kindly check your exstensions, probably you used similar solution for keyboard dismiss.