Swipe to delete cell does not cancel UIButton action - ios

My UITableView has the swipe to delete feature enabled. Each cell has a UIButton on it that performs an action (in this case, perform a segue).
I'd expect that if I swipe the cell by touching the button, the button's action would be canceled/ignored, and only the swipe would be handled. What actually happens, however, is that both gestures (swipe + tap) are detected and handled.
This means that if I just want to delete one cell and "accidentally" swipe by touching the button, the app will go to the next screen.
How can I force my app to ignore the taps in this case?

august's answer was nice enough for me, but I figured out how to make it even better:
Checking if the table was on edit mode to decide if the button should perform its action will make it behave as it should, but there will still be an issue in the user experience:
If the user wants to exit the editing mode, he should be able to tap anywhere in the cell to achieve that, including the button. However, the UIButton's action is still analyzed first by the app, and tapping the button will not exit editing mode.
The solution I found was to disable the button's user interaction while entering edit mode, and reenabling it when it's done:
// View with tag = 1 is the UIButton in question
- (void)tableView:(UITableView *)tableView willBeginEditingRowAtIndexPath:(NSIndexPath *)indexPath {
[(UIButton *)[[tableView cellForRowAtIndexPath:indexPath] viewWithTag:1] setUserInteractionEnabled:NO];
}
- (void)tableView:(UITableView *)tableView didEndEditingRowAtIndexPath:(NSIndexPath *)indexPath {
[(UIButton *)[[tableView cellForRowAtIndexPath:indexPath] viewWithTag:1] setUserInteractionEnabled:YES];
}
This way, dragging the button to enter edit mode will not trigger the button's action, and taping it to exit edit mode will indeed exit edit mode.

One elegant way would be to ignore button taps as long as a cell has entered editing mode. This works because the swipe to delete gesture will cause willBeginEditingRowAtIndexPath to be called before the button tap action is invoked.
- (void)tableView:(UITableView*)tableView willBeginEditingRowAtIndexPath:(NSIndexPath *)indexPath
{
self.isEditing = YES;
}
- (void)tableView:(UITableView*)tableView didEndEditingRowAtIndexPath:(NSIndexPath *)indexPath
{
self.isEditing = NO;
}
// button tapped
- (IBAction)tap:(id)sender
{
if (self.isEditing) {
NSLog(#"Ignore it");
}
else {
NSLog(#"Tap");
// perform segue
}
}

It's possible to do it also in your cell's subclass:
override func setEditing(editing: Bool, animated: Bool) {
super.setEditing(editing, animated: animated)
actionButton?.userInteractionEnabled = !editing
}

Because my function being called from a button press was a delegate onto my main UITableViewController's class and connected to the UITableViewCell as an IBAction, the button in my UITableViewCell was still firing on a swipe that had the UIButton pressed as part of the swipe.
In order to stop that I used the same UITableView delegates as the accepted answer, but had to set a file level variable to monitor if editing was occurring.
// in custom UITableViewCell class
#IBAction func displayOptions(_ sender: Any) {
delegate?.buttonPress()
}
// in UITableViewController class that implemented delegate
fileprivate var cellSwiped: Bool = false
func tableView(_ tableView: UITableView, willBeginEditingRowAt indexPath: IndexPath) {
cellSwiped = true
}
func tableView(_ tableView: UITableView, didEndEditingRowAt indexPath: IndexPath?) {
cellSwiped = false
}
func displayContactsSheet(for contact: Contact) {
if cellSwiped {
return
}
// proceed with delegate call button press actions
}

Related

Changing background Color of TableViewCell while editing Cells

I have a tableView with a few cells that have a black background color. On tapping the Edit Button in the navigation Bar, I would like to change the background color of area that allows re-ordering to black instead of the current white color that it shows?
I am able to change the edit button to Done using the function below but I am not sure how to change that specific area of the cells. I sense this is where I change it but i am not show how.
override func setEditing (editing:Bool, animated:Bool)
{
super.setEditing(editing,animated:animated)
if (self.editing) {
self.editButton.title = "Done"
self.navigationItem.setHidesBackButton(true, animated: true)
} else {
self.editButton.title = "Edit"
self.navigationItem.setHidesBackButton(false, animated: false)
}
}
This is the image of what I am referring to.
- (void)tableView:(UITableView *)tableView willBeginEditingRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
cell.backgroundColor = [UIColor redColor];
}
In this "swipe to delete" mode the table view does not display any insertion, deletion, and reordering controls. This method gives the delegate an opportunity to adjust the application's user interface to editing mode.
according to the comment you want to capture the event when user does not delete the cell but swipe to end the editing mode. For this, we have the following delegate:
- (void)tableView:(UITableView *)tableView didEndEditingRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
cell.backgroundColor = originalColor;
}
public class MyTableSource : UITableViewSource
{
public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
{
// abbreviation
// :
// :
cell.BackgroundColor = UIColor.Black;
return cell;
}
}
It turns out setting the cell's background color in code i.e. cellForRowAtIndexPath got the color to uniformly change to black.
I had originally done this from the UITableViewCell XIB file used.

Dissmissing Keyboard without Calling didSelectRowAtIndexPath

I have a ViewController which has textSearchTableView: UITableView and searchBar: UISearchBar
I added UITapGestureRecognizer to dissmis the keyboard
override func viewDidLoad() {
// ...
self.tap = UITapGestureRecognizer(target: self, action: "DissmissKeyboard")
self.tap.delegate = self
self.view.addGestureRecognizer(self.tap)
// ...
}
func DissmissKeyboard()
{
view.endEditing(true)
}
I have added this function to prevent breaking (didSelectRowAtIndexPath) function after selecting the cell
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldReceiveTouch touch: UITouch) -> Bool {
if touch.view.isDescendantOfView(self.textSearchTableView) {
return false
}
return true
}
But the problem is: when the keyboard is enabled and i want to dissmiss it,
if i click on the textSearchTableView, (didSelectRowAtIndexPath) will run
How can i dissmiss the keyboard if i click on the tableView without calling (didSelectRowAtIndexPath) ? and I don't want to break this function as well
I hope that I describe the problem well
Thanks a lot
Re-write your didSelectRowAtIndexPath like this:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
if (self.searchBar.isFirstResponder())
{
self.searchBar.resignFirstResponder()
}
else
{
//Do something here
}
}
Alternatively: We usually use this approach in tableviews.
self.tableView.keyboardDismissMode = .OnDrag
This will dismiss keyboard when a drag begins in the tableview.
How about you have your tableview's delegate return nil for
- (NSIndexPath *)tableView:(UITableView *)tableView
willSelectRowAtIndexPath:(NSIndexPath *)indexPath
As willSelectRowAtIndexPath is probably what you want but not what you asked for: dismissing the keyboard is done by sending the first responder resignFirstResponder, for sake of answering the actual question.

How to prevent clicking UITableView second time

My app calls a block in tableView:didSelectRowAtIndexPath and in the block it presents a view controller. If I click the cell second time when the first click is in progress, it crashes.
How can I prevent the cell to be clicked second time?
- (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
[dataController fetchAlbum:item
success:^(Album *album) {
...
...
[self presentViewController:photoViewController animated:YES completion:nil];
}];
At the beginning of didSelectRow, turn off user interaction on your table.
- (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
tableView.userInteractionEnabled = NO;
...
You may want to turn it back on later in the completion of fetchAlbum (Do this on the main thread) so that if the user comes back to this view (or the fetch fails), they can interact with the table again.
For swift 3 :
When user select a row, turn off user interactions :
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.isUserInteractionEnabled = false
Don't forget to turn it on back whenever the view appear :
override func viewDidAppear(_ animated: Bool) {
tableView.isUserInteractionEnabled = true
}
You could either prevent multiple clicks (by disabling the table or covering it up with a spinner) or you could make didSelectRowAtIndexPath present your view controller synchronously and load your "album" after it's been presented. I'm a fan of the latter as it makes the UI feel more responsive.
For a cleaner approach:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
defer {
tableView.isUserInteractionEnabled = true
}
tableView.isUserInteractionEnabled = false
}
This way you're much less prone to errors due to forgetfulness.
You want to disable user interaction on the cell:
- (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell* cell = [tableView cellForRowAtIndexPath:indexPath];
cell.userInteractionEnabled = NO;
As Stonz2 points out, you probably want to do it to the entire tableview though, rather than the specific cell if you're presenting a VC.
let cell = tableView.cellForRowAtIndexPath(indexPath)
cell?.userInteractionEnabled = false
then
set back to true before you navigate to another view, otherwise when you return back to your table the cell will be still disabled.
I have a similar approach like Skaal answered but in a different way. This solution will work for any swift version .
Create a property named isPresentingVC in your view controller and set it to true.
var isPresentingVC: Bool = true
Inside didSelect row, try this
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if isPresentingVC {
isPresentingVC = false
//do your work like go to another view controller
}
}
Now in viewWillAppear or viewDidDisappear reset its value to true
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
isPresentingVC = true
}

accessoryButtonTappedForRowWithIndexPath: not getting called

I am creating a Detail disclosure button which is getting populated using an array.... However the accessoryButtonTappedForRowWithIndexPath: function is not being called in my class. It is a TableviewDelegate and TableviewDatasource delegate.
- (void)tableView:(UITableView *)tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath{
NSLog(#"reaching accessoryButtonTappedForRowWithIndexPath:");
[self performSegueWithIdentifier:#"modaltodetails" sender:[self.eventsTable cellForRowAtIndexPath:indexPath]];
}
The NSLog isnt printing to console which leads me to believe the function isnt being called... This is of course when I select on a cell. A screenshot below shows how I have my cell setup.
The doc says that the method tableView:accessoryButtonTappedForRowWithIndexPath: is not called when an accessory view is set for the row at indexPath. The method is only called when the accessoryView property is nil and when one uses and set the accessoryType property to display a built-in accessory view.
As I understand it, accessoryView and accessoryType are mutually exclusive. When using accessoryType, the system will call tableView:accessoryButtonTappedForRowWithIndexPath: as expected, but you have to handle the other case by yourself.
The way Apple does this is shown in the Accessory sample project of the SDK. In the cellForRowAtIndexPath method of the dataSource delegate, they set a target/action to a custom accessory button. Since one can't pass the indexPath to the action, they call an auxiliary method to retrieve the corresponding indexPath and they pass the result to the delegate method:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
...
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
...
// set the button's target to this table view controller so we can interpret touch events and map that to a NSIndexSet
[button addTarget:self action:#selector(checkButtonTapped:event:) forControlEvents:UIControlEventTouchUpInside];
...
cell.accessoryView = button;
return cell;
}
- (void)checkButtonTapped:(id)sender event:(id)event{
NSSet *touches = [event allTouches];
UITouch *touch = [touches anyObject];
CGPoint currentTouchPosition = [touch locationInView:self.tableView];
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint: currentTouchPosition];
if (indexPath != nil){
[self tableView: self.tableView accessoryButtonTappedForRowWithIndexPath: indexPath];
}
}
For some reason, your setup seems to fall in the accessoryView case. Have you tried to set the accessoryType with code instead of using the Interface Builder ?
This seems very relevant...
Disclosure indicator: When this element is present, users know they can tap anywhere in the row to see the next level in the hierarchy or the choices associated with the list item. Use a disclosure indicator in a row when selecting the row results in the display of another list. Don’t use a disclosure indicator to reveal detailed information about the list item; instead, use a detail disclosure button for this purpose.
Detail disclosure button: Users tap this element to see detailed information about the list item. (Note that you can use this element in views other than table views, to reveal additional details about something; see “Detail Disclosure Buttons” for more information.) In a table view, use a detail disclosure button in a row to display details about the list item. Note that the detail disclosure button, unlike the disclosure indicator, can perform an action that is separate from the selection of the row. For example, in Phone Favorites, tapping the row initiates a call to the contact; tapping the detail disclosure button in the row reveals more information about the contact.
Converting the top answer to Swift 3:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
...
let unseletcedImage = UIImage(named: "<imagename>")
let seletcedImage = UIImage(named: "<imagename>")
let button = UIButton(type: .custom)
// match the button's size with the image size
let frame = CGRect(x: CGFloat(0.0), y: CGFloat(0.0), width: CGFloat((unseletcedImage?.size.width)!), height: CGFloat((unseletcedImage?.size.height)!))
button.frame = frame
button.setBackgroundImage(unseletcedImage, for: .normal)
button.setBackgroundImage(seletcedImage, for: .selected)
cell?.accessoryView = button
let action = #selector(checkButtonTapped(sender:event:))
(cell?.accessoryView as? UIButton)?.addTarget(self, action: action, for: .touchUpInside)
....
return cell!
}
#objc func checkButtonTapped(sender: UIButton?, event: UIEvent) {
let touches = event.allTouches
let touch = touches!.first
guard let touchPosition = touch?.location(in: self.tableView) else {
return
}
if let indexPath = tableView.indexPathForRow(at: touchPosition) {
tableView(self.tableView, accessoryButtonTappedForRowWith: indexPath)
}
}
According to UITableViewCellAccessoryType's documentation it is expected behaviour:
typedef enum : NSInteger {
UITableViewCellAccessoryNone,
UITableViewCellAccessoryDisclosureIndicator,
UITableViewCellAccessoryDetailDisclosureButton,
UITableViewCellAccessoryCheckmark,
UITableViewCellAccessoryDetailButton } UITableViewCellAccessoryType;
Constants UITableViewCellAccessoryNone The cell does not have any
accessory view. This is the default value.
UITableViewCellAccessoryDisclosureIndicator The cell has an accessory
control shaped like a chevron. This control indicates that tapping the
cell triggers a push action. The control does not track touches.
UITableViewCellAccessoryDetailDisclosureButton The cell has an info
button and a chevron image as content. This control indicates that
tapping the cell allows the user to configure the cell’s contents. The
control tracks touches.
UITableViewCellAccessoryCheckmark The cell has a check mark on its
right side. This control does not track touches. The delegate of the
table view can manage check marks in a section of rows (possibly
limiting the check mark to one row of the section) in its
tableView:didSelectRowAtIndexPath: method.
UITableViewCellAccessoryDetailButton The cell has an info button
without a chevron. This control indicates that tapping the cell
displays additional information about the cell’s contents. The control
tracks touches.
Did you just click on the cell to select it or did you actually click on the accessory button indicator on the cell? It isn't clear from your question.
accessoryButtonTappedForRowWithIndexPath is applicable when you click on the button icon within the cell and not when you select the cell.
drew is spot on.
If you change to 'DetailDisclosure' in Storyboard, then the method will fire. (xCode 4.6 DP3)
My tableView's accessoryButtonTappedForRowWithIndexPath(…) was not being called when the Detail disclosure button was hit. I eventually found that the problem was simply that tableView delegate had not been set:
self.tableView.delegate = self
This is another way to handle the disclosure button using the storyboard. You should click the table view cell and goto the Connections Inspector. There is a section called Triggered Segues, and you can drag from the selection line to the UIViewController that you want to segue to. The segue will happen automatically and you can capture prepareForSegue to capture the notification when it does.
The function accessoryButtonTappedForRowWithIndexPath never gets called for the disclosure button. It only gets called for the detailed disclosure button.
Tested on Swift 5.7 / iOS 16 (obviously would work in much earlier versions)
This solution makes use of nested functions to keep handlers and setup local to limited use cases.
extension MyViewController : UITableViewDelegate {
#objc func findRowOfTappedButton(sender: UIButton?, event: UIEvent) {
let touches = event.allTouches
let touch = touches!.first
guard let touchPosition = touch?.location(in: self.tableView) else { return }
if let indexPath = tableView.indexPathForRow(at: touchPosition) {
tableView(self.tableView, accessoryButtonTappedForRowWith: indexPath)
}
}
// Note: By implementing the following handler as appropriate delegate method,
// the scheme will still work if accessoryType changes to OS-provided type.
func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) {
tableView.delegate?.tableView!(tableView, didSelectRowAt: indexPath)
// Do whatever when button at this row is pressed.
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 44
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// Do whatever when row is selected w/o button press.
}
}
extension MyViewController : UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return myContent.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
func addAccessoryButtonToCell(_ cell : UITableViewCell) {
let image = UIImage(systemName: "paintpalette")
let button = UIButton(type: .custom)
button.frame = CGRect(x:0, y:0, width: 20, height: 20)
button.setImage(image, for: .normal)
button.addTarget(self, action: #selector(findRowOfTappedButton(sender:event:)), for: .touchUpInside)
cell.accessoryView = button
}
let cell = tableView.dequeueReusableCell(withIdentifier: "MyReusableCell", for: indexPath)
addAccessoryButtonToCell()
content.attributedText = NSAttributedString(string: myContent[indexPath.row], attributes: myAttributes[indexPath.row])
}
}

UITableView delete button not appearing

When I press the edit button I get the round delete icon to the left of the item. When I press the delete icon in the cell it 'turns' but the delete button does not show up so my commitEditingStyle never gets called because I have no delete button to press.
Just for fun...I change the cell to Insert I get the plus icon...I press it and commitEditingStyle is called.
I do not understand why I am not getting the delete button.
I have a UIViewController that I am showing in a popover. I am adding a UITableView to like so...
audioTable = [[UITableView alloc] initWithFrame:CGRectMake(0, 44, self.view.frame.size.width, 303)];
audioTable.delegate = self;
audioTable.dataSource = self;
[self.view addSubview:audioTable];
I am using a custom cell with two labels in it to display text.
Here is the custom cell initWithFrame...
primaryLabel = [[UILabel alloc]initWithFrame:CGRectMake(25 ,8, 275, 25)];
primaryLabel.font = [UIFont systemFontOfSize:14];
secondaryLabel = [[UILabel alloc]initWithFrame:CGRectMake(25 ,28, 275, 25)];
secondaryLabel.font = [UIFont systemFontOfSize:12];
[self.contentView addSubview:primaryLabel];
[self.contentView addSubview:secondaryLabel];
[self.contentView sendSubviewToBack:primaryLabel];
[self.contentView sendSubviewToBack:secondaryLabel];
I have a delete button in a toolbar in the view controller that is hooked up to the edit call. Here is what I am doing in the edit call which is getting called fine because I am getting the delete symbol in the cell...
if([self.audioTable isEditing]) {
[button setTitle:#"Edit"];
[super setEditing:NO animated:NO];
[self.audioTable setEditing:NO animated:YES];
} else {
[button setTitle:#"Done"];
[super setEditing:YES animated:NO];
[self.audioTable setEditing:YES animated:YES];
}
I have implemented the following...
-(UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath {
return UITableViewCellEditingStyleDelete;
}
-(BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
//i don't think i need to implement this really
return YES;
}
-(void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
//do delete stuff
}
}
Like I said everything is working normally, button presses and all work...just no delete button.
I had to work around it by using a different mechanism. I did a test and when I used a UITableViewController it worked fine. When I added a UITableView to a UIViewController, implementing the same thing as the UITableViewController does, it does not work. I do not know what I missed, but using the UIViewController as opposed to the UITableViewController caused the delete button to not appear.
I ran into the same issue. The problem was that my tableView's frame was not being properly resized inside the popover. In reality the delete button was being displayed but it was outside the bounds of the popover so it couldn't be seen.
I fixed this by ensuring the tableView resizes appropriately. For me this meant setting the tableView's autoresizingMask like this:
self.tableView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
The reason others were able to fix this by switching to a UITableViewController is because its tableView is properly resized.
I know you fixed it using a UITableViewController but I couldn't in my case.
Looking around I just found the answer here, you need more plumbing to get the job done. Fortunately its pretty easy. Add this to the UIViewController containing a reference to your UITableView
// objc
- (void)setEditing:(BOOL)editing animated:(BOOL)animated {
[super setEditing:editing animated:animated];
[tableView setEditing:editing animated:animated];
}
// swift
override func setEditing(editing: Bool, animated: Bool) {
super.setEditing(editing, animated: animated)
self.tableView.setEditing(editing, animated: animated)
}
The problem is that you're adding the labels using addSubview: to the cell's content view, and as they're added as last, they hide the other parts of the cell. Push them into the background by calling:
[cell.contentView sendSubviewToBack:label];
Remember to set the style of the delete button
-(UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath
{
return UITableViewCellEditingStyleDelete;
}
I had the same issue when using the UIViewController.editButtonItem in the button bar (which automatically changes the editing property of the view controller). As I was using a UIViewController with a UITableView, I needed to delegate the setEditing to the tableview, e.g.
override func setEditing(_ editing: Bool, animated: Bool) {
super.setEditing(editing, animated: animated)
tableView.setEditing(editing, animated: animated)
}
If you are using custom tableview not the TVController. You must set atleast three delegates
Swift 4.2
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .insert {
} else if editingStyle == .delete {
}
}
func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
return .delete
}
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
And most important is to add 'tableView.isEditing = true' in the viewDidload :)

Resources