Accessory image disappears when scrolling UITableView on IOS - ios

I am trying to insert a single custom image as the accessoryView for the cells of a UITableView. Yet I have a funny effect: at first the icon is inserted any two cells, then when I scroll up, all of the icons disappear and the image just shows on the bottom cell, to disappear as soon as the next one is shown scrolling. This is the code I am using:
NearCell *cell = [myTableView dequeueReusableCellWithIdentifier:CellIdentifier];
nearContents* contents=[[self sourceForTableKind:tableView==self.myTableView favorites:NO] objectAtIndex:indexPath.row];
cell.bus.text=contents.bus;
cell.destination.text=contents.destination;
cell.selectionStyle=UITableViewCellAccessoryDetailDisclosureButton;
cell.accessoryView=[[UIImageView alloc] initWithImage:[UIImage imageNamed:#"pinPoint"]];
return cell;
While in the UITableViewCell subclass nearCell I have:
- (void)layoutSubviews {
[super layoutSubviews];
CGRect accessoryFrame = self.accessoryView.frame;
accessoryFrame.size.height = self.frame.size.height;
accessoryFrame.size.width = self.frame.size.height;
accessoryFrame.origin.y=self.frame.origin.y+21;
accessoryFrame.origin.x=self.frame.origin.x+self.frame.size.width- accessoryFrame.size.width-5;
self.accessoryView.frame = accessoryFrame;
}
I also tried to allocate the accessoryView lazily, thinking the problem could have been with the repeated allocations, but if I use the same image in all the cells the image is not shown altogether.
That is how the thing shows, the icon is just in the last cell :
:
I also installed it in another tableView and there, for some reason, the layoutSubview of the subclass is not called and the accessoryViews regularly appears on all cells, albeit at a wrong size. Thus the problem seems resting in the layoutSubview callback, even if I ignore what it might be.
The problem in both cases, though, is that the accessoryButtonTappedForRowWithIndexPath callback is not called upon touching the image, rather the didSelectRowAtIndexPath callback is invoked instead.

In fact when I dispensed of the layoutSubviews callback (by manually resizing the image), all the images are correctly shown even on the original table. The only persistent problem is that the accessoryButtonTappedForRowWithIndexPath is not called upon clicking the image.

I found this solution to also solve the accessory callback problem:
Using a custom image for a UITableViewCell's accessoryView and having it respond to UITableViewDelegate
I am actually managing the issue with a block in order to pass some internal parameters. This is the final solution:
nearContents* contents=[[self sourceForTableKind:tableView==self.myTableView favorites:NO] objectAtIndex:indexPath.row];
cell.bus.text=contents.bus;
cell.destination.text=contents.destination;
cell.selectionStyle=UITableViewCellAccessoryDetailDisclosureButton;
cell.accessoryView=button;
[self registerButton:button blockForKind:tableView==self.myTableView favorites:NO];
-(void) registerButton:(UIButton*)button blockForKind:(BOOL)normal favorites:(BOOL)favorites{
[button addEventHandler:^(id sender, id event) {
NSSet *touches = [event allTouches];
UITouch *touch = [touches anyObject];
CGPoint currentTouchPosition = [touch locationInView:self.myTableView];
NSIndexPath *indexPath = [self.myTableView indexPathForRowAtPoint: currentTouchPosition];
if (indexPath != nil)
{
[self tableView: self.myTableView accessoryButtonTappedForRowWithIndexPath: indexPath];
}
}
forControlEvents:UIControlEventTouchUpInside];
}

Related

UITableView will not respond tap after scroll horizontally

I have a UITableView which is embedded in the root view of the ViewController. This UITableView contains multiple columns. And these columns can not be shown in one screen, so I make the UITableView can scroll horizontally. Here is the code which make the tableview scrolling horizontally:
- (void)viewDidAppear:(BOOL)animated
{
CGSize size = self.tableView.contentSize;
size.width = 450;
self.tableView.contentSize = size;
[self.tableView layoutIfNeeded];
}
This works fine and the table can scroll horizontally and vertically. And in the original screen(where no scroll happening), I can select one row and the selected row can be highlighted.
What my problem is: If I scroll the UITableView a little bit left, and in the area where is not shown before scrolling, the tap event cannot be responded by the UITableView. And thus, the row under my finger will not be selected and highlighted. But I can scroll if I swipe in the same area.
Anyone knows what I am doing wrong? Appreciated for your comments and thanks in advance.
I understand your problem and there is always a simple way to do a complex problem.Your required functionality can be achieved by using UICollectionView.
In your UITableViewCell add UICollectionView.In this approach your code will get cleaner and you can achieve your functionality without any headache.
Here is very simple tutorial about UICollectionView.
http://www.appcoda.com/ios-programming-uicollectionview-tutorial/
At last, I think this is a bug of UITableView. I get a workaround for the problem by subclass UITableView. Here it is:
// in my own UITableView subclass .m file
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(#"MonthDetailTableView: touchesEnded");
UITouch * touch = [touches anyObject];
CGPoint point = [touch locationInView:self];
if (point.x < self.frame.size.width)
{
[super touchesEnded:touches withEvent:event];
}
else
{
// if not adjust by mod, the NSIndexPath returned by indexPathForRowAtPoint
// will be nil. I think this is a bug of UITableView
point.x = (int)point.x % (int)self.frame.size.width;
NSIndexPath *path = [self indexPathForRowAtPoint:point];
[self selectRowAtIndexPath:path animated:NO scrollPosition:UITableViewScrollPositionNone];
if (self.delegate != nil &&
[self.delegate respondsToSelector:#selector(tableView:didSelectRowAtIndexPath:)])
{
[self.delegate tableView:self didSelectRowAtIndexPath:path];
}
}
}

Change values of UITableViewCell on tap

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!

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 get the cell reference from a gesture of another item in the cell

I have a UITableView which has some custom cells. Within the custom cells I have a main UIImageView. When the cell is created I add a Tap Gesture Recogniser to the image view.
When the image is tapped I run the following:
- (void) handleImageTap:(UIGestureRecognizer *)gestureRecognizer {
NSLog(#"Image tapped");
UIImageView *imageView = (UIImageView *)gestureRecognizer.view;
// send the image instead of self when firing the segue
[self performSegueWithIdentifier:#"remindMeTurnInfo" sender:imageView];
}
I then pass the image into a new view controller in prepareForSegue method:
if ([segue.identifier isEqualToString:#"remindMeTurnInfo"]) {
UIImageView *imgView = (UIImageView *)sender;
MESPlayedTurnReminderViewController *VC = segue.destinationViewController;
VC.turnImage = imgView.image;
}
Question
1. I need a reference to the cell that the UIImageView is within, that was tapped. So the user taps one of the cells images, and I need to know which cell (indexPath) that image was tapped from. 2. I am finding that sometimes when the image is tapped the didSelectRowAtIndexPath is being called. This is incorrect and it should not be called, only the relevant handleTap method from the Gesture Rec. should be called. How can I ensure that the didSelectRowAtIndexPath is not called, as I need to run some other code when the cell is actually (correctly) selected.
There are two ways this is commonly done. Either add a tag to your image view in cellForRowAtIndexPath that's equal to indexPath.row, or search up through the hierarchy of views from the image view until you find one that's a UITableViewCell (or subclass). That can be done like this:
- (void) handleImageTap:(UIGestureRecognizer *)gestureRecognizer {
UIImageView *imageView = (UIImageView *)gestureRecognizer.view;
UIView *superview = imageView.superview;
while (![superview isKindOfClass:[UITableViewCell class]]) {
superview = superview.superview;
}
NSLog(#"indexPath.row is: %d", [self.tableView indexPathForCell:(UITableViewCell *)superview].row);
}
The reason to search rather than using something like imageView.superview.superview (which would work in iOS 6), is that the hierarchy can change in different versions of iOS, and in fact, it did change between iOS 6 and iOS 7.
As for your second question, it probably happens because you're accidentally tapping the cell rather than the image view. Other than making the image view larger so it's easier to tap, I don't see a fix for that.
1.- To get the cell you can check the superview of the UIImageView tapped
- (void) handleImageTap:(UIGestureRecognizer *)gestureRecognizer
{
UIImageView *imageView = (UIImageView *)gestureRecognizer.view;
UITableViewCell *cell = (UITableViewCell *)imageView.superview;
}
2.- To disable the cell selection on a UITableView
[self.yourTableView setAllowsSelection:NO];

Identifying the table cell, when tapping on a Tap Recogniser

I have a tableview in my View. The cells are created using custom Cells. I need to display a large string in the table view cells So I had added the text Label in a Scrollview. Also I need to execute some code when the user taps on table view cell. Please see the below code:
[cell.textLabelLine2 setFrame:CGRectMake(cell.textLabelLine2.frame.origin.x, cell.textLabelLine2.frame.origin.y, 500, cell.textLabelLine2.frame.size.height)];
cell.scrollView.contentSize = CGSizeMake(cell.textLabelLine2.text.length*10 , 10);
cell.scrollView.pagingEnabled = NO;
The problem is when the user touches above the Scroll View, the Tableview did select method will not be called. The solution I found for this problem is to add a gesture recogniser to the scroll view. But in this solution, we have no ways to check which cell(or which gesture recogniser) was selected. Could anyone help me to find a solution for this problem?
You can get to know the cell by the following code
if(gestureRecognizer.state == UIGestureRecognizerStateBegan) {
CGPoint p = [gestureRecognizer locationInView:[self tableView]];
NSIndexPath *indexPath = [[self tableView] indexPathForRowAtPoint:p];
if(indexPath != nil) {
UITableViewCell *cell = [[self tableView] cellForRowAtIndexPath:indexPath];
...
}
}
It's generally a bad idea putting scroll views inside scroll views. UITableView is also just a UIScrollView. That only kind of works if they are scrolling on different axis, i.e. the outer scroll view scrolling vertically and the inner scrolling horizontally.
For your specific scenario you would have to trigger the selection yourself. Once you have a reference to the cell you can ask the table view for the indexPath of it. Then you would call the delegate method for didSelectRow... yourself.
In the solution with the scrollview you are not able to scroll in the scrollview because the gestureRecognizer 'gets' the touch. Therefor I would not use the scrollview at all.
Make the label resize to its content like:
CGSize customTextLabelSize = [cell.customTextLabel.text sizeWithFont:cell.customTextLabel.font constrainedToSize:CGSizeMake(cell.customTextLabel.frame.size.width, 999999)];
cell.customTextLabel.frame = CGRectMake(cell.customTextLabel.frame.origin.x, cell.customTextLabel.frame.origin.y, cell.customTextLabel.frame.size.width, customTextLabelSize.height);
You also need to implement this in the heightForRowAtIndexPath
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
CGSize cellSize = [bigTextString sizeWithFont:customTextLabel.font constrainedToSize:CGSizeMake(generalCellWidth, 999999)];
return cellSize.height;
}
This way you can just use the didSelectRowAtIndex method.
If you really want to use the scrollview, add a button to your cell in the cellForRowAtIndexPath: method. Make the button just as big as the cell and add a button tag like this:
UIButton *cellButton = [UIButton buttonWithType:UIButtonTypeCustom];
cellButton.frame = CGRectMake(0, 0, cell.frame.size.width, cell.frame.size.height);
cellButton.tag = indexPath.row;
[cellButton addTarget:self action:#selector(cellButtonAction:) forControlEvents:UIControlEventTouchUpInside];
[cell.contentView addSubview:cellButton];
Then add:
-(void)cellButtonAction:(UIButton*)sender
{
//do something with sender.tag
}

Resources