Change values of UITableViewCell on tap - ios

I need to change the value inside a UITableViewCell when the user taps on it.
I need to modify the value trough an animation of a value inside a UITableViewCell.
Right now, I've implemented a UITapGestureRecognizer when the user taps on the UILabel, like so:
UITapGestureRecognizer *tapOnAmount = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tapOnBalance)];
[cell.amountLabel setUserInteractionEnabled:YES];
[cell.amountLabel addGestureRecognizer:tapOnAmount];
Changing the values in a method didTapOnBalance will crash the app, like so:
-(void)tapOnBalance{
NSString *headerIdentifier = #"HeaderCell";
HeaderTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:headerIdentifier];
cell.amountLabel.text = #"new Value"; // this will crash because at runtime
// the compiler won't recognize cell.amountLabel...
}
Implementing this in the UITableViewCell will cause me to send the values of the HeaderTableViewCell to the subclass and I don't know how to do that either.

You can't just deque a new cell, that will not give you the cell that the user tapped - it will make a new one. But, if you change your tap handler just a little, you can get the index path of the cell tapped from the gesture.
You need a slight change to the initialization of the gesture (look at the selector):
UITapGestureRecognizer *tapOnAmount = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tapOnBalance:)];
[cell.amountLabel setUserInteractionEnabled:YES];
[cell.amountLabel addGestureRecognizer:tapOnAmount];
and then another slight change to your handler:
- (void)tapOnBalance:(UITapGestureRecognizer *)sender
{
CGPoint location = [sender locationInView:self.view];
CGPoint locationInTableview = [self.tableView convertPoint:location fromView:self.view];
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:locationInTableview];
// then you can either use the index path to call something like configureCell or send a didSelectRowAtIndexPath like this:
[self tableView:self.tableView didSelectRowAtIndexPath:indexPath];
}

Your code is totally wrong.
You're creating a new cell when the user taps on an existing cell, and trying to change the value displayed in that new cell. Don't do that.
Instead, change the data in your table view's data model, then tell your table view to reload that cell (as explained in ZAZ's answer.) If you've changed the data model to reflect new info for your cell, reloading it will cause it to be displayed with the new settings.

you must implement didSelecRowAtIndexPath and write in it the following line of code after the changing the value to animate the tapped row
[self.myTableView reloadRowsAtIndexPaths:indexPath] withRowAnimation:UITableViewRowAnimationNone];
Hope it helps!

Related

Correct way to setting a tag to all cells in TableView

I'm using a button inside a tableView in which I get the indexPath.row when is pressed. But it only works fine when the cells can be displayed in the screen without scroll.
Once the tableView can be scrolleable and I scrolls throught the tableview, the indexPath.row returned is a wrong value, I noticed that initially setting 20 objects, for example Check is just printed 9 times no 20.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
lBtnWithAction = [[UIButton alloc] initWithFrame:CGRectMake(liLight1Xcord + 23, 10, liLight1Width + 5, liLight1Height + 25)];
lBtnWithAction.tag = ROW_BUTTON_ACTION;
lBtnWithAction.titleLabel.font = luiFontCheckmark;
lBtnWithAction.tintColor = [UIColor blackColor];
lBtnWithAction.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
[cell.contentView addSubview:lBtnWithAction];
}
else
{
lBtnWithAction = (UIButton *)[cell.contentView viewWithTag:ROW_BUTTON_ACTION];
}
//Set the tag
lBtnWithAction.tag = indexPath.row;
//Add the click event to the button inside a row
[lBtnWithAction addTarget:self action:#selector(rowButtonClicked:) forControlEvents:UIControlEventTouchUpInside];
//This is printed just 9 times (the the number of cells that are initially displayed in the screen with no scroll), when scrolling the other ones are printed
NSLog(#"Check: %li", (long)indexPath.row);
return cell;
}
To do something with the clicked index:
-(void)rowButtonClicked:(UIButton*)sender
{
NSLog(#"Pressed: %li", (long)sender.tag);
}
Constants.h
#define ROW_BUTTON_ACTION 9
What is the correct way to get the indexPath.row inside rowButtonClicked or setting a tag when I have a lot of of cells in my tableView?
My solution to this kind of problem is not to use a tag in this way at all. It's a complete misuse of tags (in my opinion), and is likely to cause trouble down the road (as you've discovered), because cells are reused.
Typically, the problem being solved is this: A piece of interface in a cell is interacted with by the user (e.g. a button is tapped), and now we want to know what row that cell currently corresponds to so that we can respond with respect to the corresponding data model.
The way I solve this in my apps is, when the button is tapped or whatever and I receive a control event or delegate event from it, to walk up the view hierarchy from that piece of the interface (the button or whatever) until I come to the cell, and then call the table view's indexPath(for:), which takes a cell and returns the corresponding index path. The control event or delegate event always includes the interface object as a parameter, so it is easy to get from that to the cell and from there to the row.
Thus, for example:
UIView* v = // sender, the interface object
do {
v = v.superview;
} while (![v isKindOfClass: [UITableViewCell class]]);
UITableViewCell* cell = (UITableViewCell*)v;
NSIndexPath* ip = [self.tableView indexPathForCell:cell];
// and now we know the row (ip.row)
[NOTE A possible alternative would be to use a custom cell subclass in which you have a special property where you store the row in cellForRowAt. But this seems to me completely unnecessary, seeing as indexPath(for:) gives you exactly that same information! On the other hand, there is no indexPath(for:) for a header/footer, so in that case I do use a custom subclass that stores the section number, as in this example (see the implementation of viewForHeaderInSection).]
I agree with #matt that this is not a good use of tags, but disagree with him slightly about the solution. Instead of walking up the button's superviews until you find a cell, I prefer to get the button's origin, convert it to table view coordinates, and then ask the table view for the indexPath of the cell that contains those coordinates.
I wish Apple would add a function indexPathForView(_:) to UITableView. It's a common need, and easy to implement. To that end, here is a simple extension to UITableView that lets you ask a table view for the indexPath of any view that lies inside one of the tableView's cells.
Below is the key code for the extension, in both Objective-C and Swift. There is a working project on GitHub called TableViewExtension-Obj-C that illustrates the uses of the table view extension below.
EDIT
In Objective-C:
Header file UITableView_indexPathForView.h:
#import <UIKit/UIKit.h>
#interface UIView (indexPathForView)
- (NSIndexPath *) indexPathForView: (UIView *) view;
#end
UITableView_indexPathForView.m file:
#import "UITableView_indexPathForView.h"
#implementation UITableView (UITableView_indexPathForView)
- (NSIndexPath *) indexPathForView: (UIView *) view {
CGPoint origin = view.bounds.origin;
CGPoint viewOrigin = [self convertPoint: origin fromView: view];
return [self indexPathForRowAtPoint: viewOrigin];
}
And the IBAction on the button:
- (void) buttonTapped: (UIButton *) sender {
NSIndexPath *indexPath = [self.tableView indexPathForView: sender];
NSLog(#"Button tapped at indexpPath [%ld-%ld]",
(long)indexPath.section,
(long)indexPath.row);
}
In Swift:
import UIKit
public extension UITableView {
func indexPathForView(_ view: UIView) -> IndexPath? {
let origin = view.bounds.origin
let viewOrigin = self.convert(origin, from: view)
let indexPath = self.indexPathForRow(at: viewOrigin)
return indexPath
}
}
I added this as a file "UITableView+indexPathForView" to a test project to make sure I got everything correct. Then in the IBAction for a button that is inside a cell:
func buttonTapped(_ button: UIButton) {
let indexPath = self.tableView.indexPathForView(button)
print("Button tapped at indexPath \(indexPath)")
}
I made the extension work on any UIView, not just buttons, so that it's more general-purpose.
The nice thing about this extension is that you can drop it into any project and it adds the new indexPathForView(_:) function to all your table views without having do change your other code at all.
You are running into the issue of cell-reuse.
When you create a button for the view you set a tag to it, but then you override this tag to set the row number to it.
When the cell get's reused, because the row number is longer ROW_BUTTON_ACTION, you don't reset the tag to the correct row number and things go wrong.
Using a tag to get information out of a view is almost always a bad idea and is quite brittle, as you can see here.
As Matt has already said, walking the hierarchy is a better idea.
Also, your method doesn't need to be written in this way. If you create your own custom cell, then the code you use to create and add buttons and tags isn't needed, you can do it in a xib, a storyboard, or even in code in the class. Furthermore, if you use the dequeue method that takes the index path, you will always get either a recycled cell, or a newly created cell, so there is no need to check that the cell returned is not nil.

My gesture recognizer is attached to the wrong view

I have a UICollectionView that has elements that can be dragged and dropped around the screen. I use a UILongPressGestureRecognizer to handle the dragging. I attach this recognizer to the collection view cells in my collectionView:cellForItemAtIndexPath: method. However, the recognizer's view property occasionally returns a UIView instead of a UICollectionViewCell. I require some of the methods/properties that are only on UICollectionViewCell and my app crashes when a UIView is returned instead.
Why would the recognizer that is attached to a cell return a plain UIView?
Attaching the recognizer
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
EXSupplyCollectionViewCell *cell = (EXSupplyCollectionViewCell *)[collectionView dequeueReusableCellWithReuseIdentifier:CellIdentifier forIndexPath:indexPath];
UILongPressGestureRecognizer *longPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:cell action:nil];
longPressRecognizer.delegate = self;
[cell addGestureRecognizer:longPressRecognizer];
return cell;
}
Handling the gesture
I use a method with a switch statement to dispatch the different states of the long press.
- (void)longGestureAction:(UILongPressGestureRecognizer *)gesture {
UICollectionViewCell *cell = (UICollectionViewCell *)[gesture view];
switch ([gesture state]) {
case UIGestureRecognizerStateBegan:
[self longGestureActionBeganOn:cell withGesture:gesture];
break;
//snip
default:
break;
}
}
When longGestureActionBeganOn:withGesture is called if cell is actually a UICollectionViewCell the rest of the gesture executes perfectly. If it isn't then it breaks when it attempts to determine the index path for what should be a cell.
First occurrence of break
- (void)longGestureActionBeganOn:(UICollectionViewCell *)cell withGesture:(UILongPressGestureRecognizer *)gesture
{
NSIndexPath *indexPath = [self.collectionView indexPathForCell:cell]; // unrecognized selector is sent to the cell here if it is a UIView
[self.collectionView setScrollEnabled:NO];
if (indexPath != nil) {
// snip
}
}
I also use other properties specific to UICollectionViewCell for other states of the gesture. Is there some way to guarantee that the recognizer will always give me back the view that I assigned it to?
Views like UICollectionView and UITableView will reuse their cells. If you blindly add a gestureRecognizer in collectionView:cellForItemAtIndexPath: you will add a new one each time the cell is reloaded. If you scroll around a bit you will end up with dozens of gestureRecognizers on each cell.
In theory this should not cause any problems besides that the action of the gestureRecognizer is called multiple times. But Apple uses heavy performance optimization on cell reuse, so it might be possible that something messes up something.
The preferred way to solve the problem is to add the gestureRecognizer to the collectionView instead.
Another way would be to check if there is already a gestureRecognizer on the cell and only add a new one if there is none. Or you use the solution you found and remove the gestureRecognizer in prepareForReuse of the cell.
When you use the latter methods you should check that you remove (or test for) the right one. You don't want to remove gestureRecognizers the system added for you. (I'm not sure if iOS currently uses this, but to make your app proof for the future you might want to stick to this best practice.)
I had a similar problem related to Long-Touch.
What I ended up doing is override the UICollectionViewCell.PrepareForReuse and cancel the UIGestureRecognizers attached to my view. So everytime my cell got recycled a long press event would be canceled.
See this answer

How to add tap gesture to UICollectionViewCell subview returned from dequeueReusableCellWithReuseIdentifier

What's the best method for efficiently adding a tap gesture to a subview of a UICollectionViewCell returned from dequeueReusableCellWithReuseIdentifier that already has a bunch of default gesture recognizers attached to it (such as a UIScrollView). Do I need to check and see if my one custom gesture is already attached (scrollView.gestureRecognizers) and if not then add it? I need my app's scrolling to be as smooth as possible so performance of the check and efficient reuse of already created resources is key. This code all takes place inside cellForItemAtIndexPath. Thanks.
I figured out a way to do it that requires only a single, shared, tap gesture recognizer object and moves the setup code from cellForItemAtIndexPath (which gets called very frequently as a user scrolls) to viewDidLoad (which gets called once when the view is loaded). Here's the code:
- (void)myCollectionViewWasTapped:(UITapGestureRecognizer *)tap
{
CGPoint tapLocation = [tap locationInView:self.collectionView];
NSIndexPath *indexPath = [self.collectionView indexPathForItemAtPoint:tapLocation];
if (indexPath)
{
MyCollectionViewCell *cell = (MyCollectionViewCell *)[self.collectionView cellForItemAtIndexPath:indexPath];
CGRect mySubviewRectInCollectionViewCoorSys = [self.collectionView convertRect:cell.mySubview.frame fromView:cell];
if (CGRectContainsPoint(mySubviewRectInCollectionViewCoorSys, tapLocation))
{
// Yay! My subview was tapped!
}
}
}
- (void)viewDidLoad
{
// Invoke super
[super viewDidLoad];
// Add tap handler to collection view
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(myCollectionViewWasTapped:)];
[self.collectionView addGestureRecognizer:tap];
}
Here's a rough, very simple outline of a possible design solution: you could subclass UICollectionViewCell and override its initialization methods to add the gesture recognizer to its subviews. Furthermore, if you don't want the cell to "know" about the gesture recognizer, you could create a protocol that the data source object would conform to. The cell object would call a "setup" protocol method at the appropriate time.
Hope this helps!

How can I swipe a row from a TableView to another

I have in one ViewControl two TableViews implemented. Initially one has data and another doesn't.
So, I was wondering how would I implement the gesture event of touching one row in the fulfilled TableView and swipe it to the empty TableView, in order to move that datum.
add the swipe gesture to each cell of your table, this gesture call a Custom method where you pass the indexPath of the cell then in your Custom method write a code for add this object from the array of first table to the second and remove it from first array. Finally you refresh boh table with reloadData method
What I've done so far... (It's slightly different of what #Mirko told)
Added the UISwipeGesture to the table (inside ViewDidLoad method), and defined it to capture the "Swipe Up" event, calling my event handler.
swipeCell = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(swipeUpEvent:)];
swipeCell.direction = UISwipeGestureRecognizerDirectionUp;
[self.tableA addGestureRecognizer:swipeCell];
Implemented the event handler, that get the cell selected by the swipe movement when it starts.
-(IBAction)swipeUpEvent:(UIGestureRecognizer *) sender
{
CGPoint location = [sender locationInView:self.tableA];
NSIndexPath *swipedIndexPath = [self.tableA indexPathForRowAtPoint:location];
NSString *temp = [arrayA objectAtIndex:swipedIndexPath.row];
[arrayB addObject:temp];
[arrayA removeObjectAtIndex:swipedIndexPath.row];
[self.tableB reloadData];
[self.tableA reloadData];
}
It may not be the most elegant solution for the problem, but at least I avoided creating an ActionListener for each cell. I hope it saves me some memory.

Get a table cell when a gesture fires

I have a custom UITableViewCell that I defined in a storyboard. I added a gesture to an image within the table cell. I capture the gesture just fine and change the image.
What I need to do now is remove the tableCell from the table list, but I'm having trouble finding a reference to that table cell.
How do I get the table cell indexPath or a reference to that table cell? I don't really want to climb the superview later as it seems to me there must be an easier/better way.
Thanks.
I actually found a solution. Using target action I did this.
-(void)checkMarkTapDetected:(id)sender {
// Update the image
UIImageView *checkMarkImage = (UIImageView *)[sender view];
checkMarkImage.image = [UIImage imageNamed:#"checkbox_checked.png"];
// Get recognizer and the place it fired in the tableview
UIGestureRecognizer *gestureRecognizer = [[checkMarkImage gestureRecognizers] lastObject];
CGPoint location = [gestureRecognizer locationInView:self.tableView];
// Get table cell index based on gesture location in table view cell
_checkMarkRowIndexPath = [self.tableView indexPathForRowAtPoint:location];
}
I see two options. You can use deleteRowsAtIndexPaths:withRowAnimation: or you can send reloadData to your tableView. reloadData will cause your delegate method cellforRowAtIndexPath (and related methods) to be called again but this time you will not create that very cell. Your numberOfRowsInSection will have to reflect the decrease of rows too.

Resources