Button on CollectionView Cell Only Receives Taps After Long Press - ios

I am programmatically adding a UIButton to a custom UICollectionViewCell. Taps on the button are only detected if I do a long press on the cell first. Then the button works as expected. I have to do the same long press on each cell individually in order to get their respective button to recognize taps.
Note that single taps (i.e. didSelectItemAtIndexPath) on cells work as expected. So does long press on a cell. It might be worth noting that UILongPressGestureRecognizer is the only gesture added to the collectionView.
UICollectionViewCell class
- (UIButton *)stackInfoButton
{
if(_stackInfoButton == nil)
{
self.stackInfoButton = [UIButton buttonWithType:UIButtonTypeInfoDark];
[self.stackInfoButton setShowsTouchWhenHighlighted:YES];
}
return _stackInfoButton;
}
- (void)layoutSubviews
{
// Other stuff here. Removed for readability
self.stackInfoFrame = CGRectMake(4.0f, self.bounds.size.height - self.stackInfoButton.bounds.size.height - 4, self.stackInfoButton.bounds.size.width, self.stackInfoButton.bounds.size.height);
}
- (void)drawRect:(CGRect)rect
{
// Other stuff here. Removed for readability
self.stackInfoButton.frame = self.stackInfoFrame;
[[self contentView] addSubview:self.stackInfoButton];
}
UICollectionViewController class
- (void)viewDidLoad
{
[super viewDidLoad];
// Other stuff here. removed for readability
self.longPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(longPress:)];
self.longPressGesture.delegate = self;
[self.collectionView addGestureRecognizer:self.longPressGesture];
self.longPressGesture.cancelsTouchesInView = NO;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
DVCardViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kDVCollectionViewCellReuseIdentifier forIndexPath:indexPath];
cell.indexPath = indexPath;
// Other stuff here. Removed for readability
[cell.stackInfoButton addTarget:self action:#selector(stackInfoButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
return cell;
}
- (void)stackInfoButtonTapped:(id)sender
{
NSLog(#">>> Entering %s <<<", __PRETTY_FUNCTION__);
}
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
NSLog (#"Touch View : %#", touch.view);
// tried both.
// DVCardViewCell *cell;
// if (touch.view == cell.stackInfoButton)
if ( [touch.view isKindOfClass:[UIButton class]] )
{
NSLog(#"Gesture on button, returns no");
return NO;
}
NSLog(#"Gesture returns outside, yes");
return YES;
}
Suggestions appreciated. I've been through many UICollectionView threads on SO. Yet to find one that addresses my issue. Thanks.

You say you have added a gesture to the collection view, so you need to teach that gesture not to steal touches that it shouldn't get involved with. Add your controller as the delegate of the gesture and implement gestureRecognizer:shouldReceiveTouch:. In that method check the view that the touch hit and, if it's a button, return NO to prevent the gesture from getting involved.

Try this
self.LongPressGesture.cancelsTouchesInView = NO;
Sorry was mistaken. never mind.
This looks really weird:
- (UIButton *)stackInfoButton
{
if(_stackInfoButton == nil)
{
self.stackInfoButton = [UIButton buttonWithType:UIButtonTypeInfoDark]; // theoretically your app should crash now
[self.stackInfoButton setShowsTouchWhenHighlighted:YES];
}
return _stackInfoButton;
}
Look at self.stackInfoButton = [UIButton buttonWithType:UIButtonTypeInfoDark];
It must be
_stackInfoButton = [UIButton buttonWithType:UIButtonTypeInfoDark];
[_stackInfoButton setShowsTouchWhenHighlighted:YES];
maybe it helps.

Related

Manage long press on UITextfiled without disabling context menu?

I've UITableView and its cell have one UITextField.
Now I added long gesture in UITextField but it not working. When I tap long gesture on textfield it always show context menu (select,copy cut,past,etc.).
My question is that how to manage long gesture as well as context menu in UITextFiled.
I've tried below code:
longGesture = [[UILongPressGestureRecognizer alloc]
initWithTarget:self action:#selector(handleLongPress:)];
longGesture.minimumPressDuration = 2.0; //seconds
longGesture.delegate = self;
-(void)handleLongPress:(UILongPressGestureRecognizer *)gestureRecognizer
{
CGPoint p = [gestureRecognizer locationInView:self.tableView];
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:p];
if (indexPath == nil) {
NSLog(#"long press on table view but not on a row");
} else if (gestureRecognizer.state == UIGestureRecognizerStateBegan) {
NSLog(#"long press on table view at row %ld", indexPath.row);
} else {
NSLog(#"gestureRecognizer.state = %ld", gestureRecognizer.state);
}
}
Tableview delegate method
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
Note *obj = [self.dataArr objectAtIndex:indexPath.row];
TableViewCell *Cell = [self.tableView dequeueReusableCellWithIdentifier:#"cell"];
if (Cell == nil) {
Cell = [[TableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"cell"];
Cell.selectionStyle = UITableViewCellSelectionStyleNone;
}
else{
Cell.memoField.text = obj.memoRowText;
}
Cell.memoField.userInteractionEnabled = YES;
[Cell.memoField addGestureRecognizer:longGesture];
Cell.memoField.delegate = self;
Cell.memoField.tag = indexPath.row;
return Cell;
}
You'll want to set up a failure requirement between the gesture that shows the context menu and your long press gesture. Specifically, you'll want the menu recognizer to require your long press to fail (i.e. you'll want the menu recognizer to wait until it has ruled out the long press). In code, one way to do that is this is implementing this delegate method.
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)longPress shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)other {
if (other.view /* is a text field in the table view */) {
return YES;
} else {
return NO;
}
}
These methods can be a little confusing.
Remember that you can add "static" failure requirements with -[UIGestureRecognizer requireGestureRecognizerToFail:], but in many cases you don't necessary easily have references to both recognizers (such as in this case).
In many cases, this suffices.
However, the gesture recognizer system also gives you a chance to install failure requirements "on the fly".
Returning YES from -gestureRecognizer:shouldBeRequiredToFailByGestureRecognizer: has the same effect as if you called [second requireFailureOfGestureRecognizer:first] (where first and second are the first and second arguments to that method).
OTOH returning YES from -gestureRecognizer:shouldRequireFailureOfGestureRecognizer: has the same effect as if you called [first requireFailureOfGestureRecognizer:second].

Buttons on UITableViewCell not working when in editing mode

I have a UITableView populated with cells that have buttons on them. I want those buttons to work also when in editing mode, but they don't. It seems like the gesture recognizer on UITableViewCell is preventing gesture recognizers on buttons. Does anyone have any suggestions about approaching this problem?
ViewController.m:
- (void)viewDidLoad {
[super viewDidLoad];
self.items = [#[#"one", #"two", #"three", #"four"] mutableCopy];
}
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
TableViewCell *cell = [tableView dequeueReusableCellWithIdentifier: #"cellID"
forIndexPath:indexPath];
if (cell == nil) {
cell = [[TableViewCell alloc] init];
}
[cell.button setTitle: self.items[indexPath.row]
forState: UIControlStateNormal];
return cell;
}
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
return YES;
}
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
[self.items removeObjectAtIndex: indexPath.row];
}
}
TableViewCell.h:
#interface TableViewCell : UITableViewCell
#property (weak, nonatomic) IBOutlet UIButton *button;
- (IBAction)buttonTapped:(id)sender;
#end
So, the buttonTapped: is called when a cell isn't in editing mode, but when a cell is in editing mode, the button doesn't work.
Use different tags from each button in the cells. For example,
Inside cellforRowAtIndexPath, specify a tag for a button.
[Button addTarget:self action:#selector(func:event:) forControlEvents:UIControlEventTouchUpInside];
[Button setTag:indexPath.row];
Now call the function and refer the cell using tag.
-(IBAction)func:(id)sender event:(id)event{
UITouch *touch = [[event allTouches] anyObject];
CGPoint Pos = [touch locationInView:self.tableview];
NSIndexPath *indexPath = [self.tableview indexPathForRowAtPoint:Pos];
if(indexPath != nil){
//do operation with indexPath
}
}
Hope it helps...
First of all see some code what you have done !! without proper declaration its difficult to give answer.
Firstly My suggestion is give gesture recognize in UItableview delegate method.
- (void)tableView:(UITableView*)tableView willBeginEditingRowAtIndexPath:(NSIndexPath *)indexPath
{
// code
}
Sometimes gesture is not working with small mistake in coding.
Secondly if possible ,
Instead of adding the Gesture recognizer to the Cell directly, you can add it to the UItableview in viewDidLoad.
This is the best way to work gesture in UITableview.
As said by Phix in Example,
In the didSwipe-Method you can determine the affected IndexPath and cell as follows:
-(void)didSwipe:(UIGestureRecognizer *)gestureRecognizer {
if (gestureRecognizer.state == UIGestureRecognizerStateEnded) {
CGPoint swipeLocation = [gestureRecognizer locationInView:self.tableView];
NSIndexPath *swipedIndexPath = [self.tableView indexPathForRowAtPoint:swipeLocation];
UITableViewCell* swipedCell = [self.tableView cellForRowAtIndexPath:swipedIndexPath];
// ...
}
}
You can set other gesture like as Above.
I hope it will solve your issue.
To do this, you need to subclass the UITableView and make it conform to a UIGestureRecognizerDelegate protocol. In .m file of your UITableView subclass add the following code:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldReceiveTouch:(UITouch *)touch {
if ([touch.view isKindOfClass: [UIButton class]]) {
return NO;
}
return YES;
}

UILongPressGestureRecognizer on collection view breaks cell highlighting

When I add a UILongPressGestureRecognizer on my collectionView (I'm doing it like in this answer), the collection view stops highlighting cells as expected.
I'm handling the cell highlighting in my UICollectionViewCellSubclass, like it's explained in this answer:
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
if (self.highlighted) {
//Here I show my highlighting layer
}
else {
//Here I hide my highlighting layer
}
}
- (void)setHighlighted:(BOOL)highlighted
{
[super setHighlighted:highlighted];
[self setNeedsDisplay]; //This call draw rect and show/hide the highlighting layer
}
The problem seems to be that, when I add the UILongPressGestureRecognizer gesture on the collection view, drawRect is not being called after setting highlighted to YES.
If I move the logic to show my highlighting layer in setHighlighted:YES, I can't see the touching-feedback because setHighlighted:NO its being called immediately after! (This happens only after adding the UILongPressGestureRecognizer).
I've also tried to move the logic in touchesBegan/touchesEnded but then didSelectRowAtIndexPath: doesn't work anymore.
Unfortunately, it looks like the UILongPressGestureRecognizer is blocking the touches required for the UICollectionView to highlight the cell. It doesn't look like there is a good way to forward the touches, so I'd imagine you will have to handle the highlighting manually if you want that to appear. You could do it through a separate method than setHighlighted, or try to use the cell.highlighted property to do your drawing.
Here is an example I came up with that worked for my UICollectionView
-(void)handleLongPress:(UILongPressGestureRecognizer *)gestureRecognizer
{
// if (gestureRecognizer.state != UIGestureRecognizerStateEnded) {
// return;
// }
CGPoint p = [gestureRecognizer locationInView:self.collectionView];
NSIndexPath *indexPath = [self.collectionView indexPathForItemAtPoint:p];
if (indexPath == nil){
NSLog(#"couldn't find index path");
} else {
// get the cell at indexPath (the one you long pressed)
UICollectionViewCell* cell = [self.collectionView cellForItemAtIndexPath:indexPath];
if (gestureRecognizer.state != UIGestureRecognizerStateEnded) {
if (!cell.highlighted) {
[cell setHighlighted:YES];
}
return;
}
[cell setHighlighted:NO];
// do stuff with the cell
}
}

Custom CheckBoxView for UITableView Duplicating the Check Marks

I have created a CheckBoxView control for my UITableViewCell. The problem I am facing is that once I checkmark one of the top rows and scrolls the same check mark is visible on the bottom rows. This is because of dequeueresuable rows feature and I want to know how can I fix it. Here is the implementation.
CheckBoxView.m:
-(instancetype) initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
[self setup];
[self registerGesturesRecognizers];
return self;
}
-(void) registerGesturesRecognizers {
UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(checkBoxTapped:)];
[self addGestureRecognizer:tapGestureRecognizer];
}
-(void) checkBoxTapped:(UITapGestureRecognizer *) recognizer {
if(self.checkBoxViewSelectionChanged) {
if(!self.isChecked) {
self.checkBoxViewSelectionChanged(self,self.isChecked);
self.isChecked = YES;
}
else {
self.checkBoxViewSelectionChanged(self,self.isChecked);
}
}
}
-(void) check {
[_checkBoxImageView setImage:[UIImage imageNamed:#"small-check"]];
}
-(void) uncheck {
_checkBoxImageView.image = nil;
}
-(void) setup {
self.userInteractionEnabled = YES;
self.layer.borderWidth = 0.5f;
self.layer.borderColor = [UIColor lightGrayColor].CGColor;
_checkBoxImageView = [[UIImageView alloc] initWithFrame:CGRectMake(1, 0, 23, 23)];
[self addSubview:_checkBoxImageView];
}
And here is the cellForRowAtIndexPath method:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
SamplesTableViewCell *cell = (SamplesTableViewCell *) [tableView dequeueReusableCellWithIdentifier:#"SamplesTableViewCell" forIndexPath:indexPath];
Item *sample = [_samples objectAtIndex:[indexPath row]];
cell.productNameLabel.text = sample.product.name;
cell.productColorLabel.text = sample.productColor.name;
[cell.productImageView setImage:sample.productColor.image];
cell.checkboxView.checkBoxViewSelectionChanged = ^(CheckBoxView *checkBoxView, BOOL isChecked) {
if(!isChecked) {
[checkBoxView check];
checkBoxView.isChecked = YES;
}
else {
[checkBoxView uncheck];
checkBoxView.isChecked = NO;
}
};
return cell;
}
The CheckBoxView is actually a UIView on the Storyboard prototype cell whose class is set to CheckBoxView so it is not created dynamically in the cellForRowAtIndexPath event. When I run the above code and checkmark the top rows and scroll then the same checkmark appears on the lower rows.
UPDATE:
Here is my updated code but it still checks and unchecks the rows at the bottom.
cell.checkboxView.checkBoxViewSelectionChanged = ^{
if(!sample.isSelected) {
[sample setSelected:YES];
[cell.checkboxView check];
}
else
{
[sample setSelected:NO];
[cell.checkboxView uncheck];
}
};
Your UIView should not be maintaining the isChecked state for your checkbox. As you pointed out, due to cell reuse all the others will then be checked because the same UIView is being used.
Your model objects powering your data need to maintain the state, or your controller.
For example:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
SamplesTableViewCell *cell = (SamplesTableViewCell *) [tableView dequeueReusableCellWithIdentifier:#"SamplesTableViewCell" forIndexPath:indexPath];
// Some code...
if ([self.myDatasource[indexPath.row] isChecked]) {
cell.checkBoxView.isChecked = YES;
}
return cell;
}
Just some rough pseudocode, but hopefully you get the idea. Potential ways to implement this state maintained is either in the form of model objects, or in your UIViewController have an NSArray containing an NSDictionary representing each row in your UITableView, where each key-value pair maintains a particular state for your UITableViewCell.

UIGestureRecognizer in table view cell get indexPath always return the indexPath of the last cell

I have a UIImageView inside UITaleViewCell, I added a tap recognizer to the UIImageView.
- (IBAction)ui_tapped:(UITapGestureRecognizer *)sender {
NSIndexPath *indexPath = [CocoaHelper indexPathWithTableView:self.tableView sender:sender.view];
RichMediaViewController *viewController = (RichMediaViewController *)[CocoaHelper viewControllerWithIdentifier:VC_RICH_MEDIA];
Message *message = self.messages[indexPath.row];
[viewController setupWithEntity:message];
[self presentViewController:viewController animated:YES completion:nil];
}
+ (NSIndexPath *)indexPathWithTableView:(UITableView *)tableView sender:(id)sender {
CGPoint hitPoint = [sender convertPoint:CGPointZero toView:tableView];
NSIndexPath *hitIndex = [tableView indexPathForRowAtPoint:hitPoint];
return hitIndex;
}
I used + (NSIndexPath *)indexPathWithTableView:(UITableView *)tableView sender:(id)sender
for buttons inside cells and it is correct, but for gesture, it always returns the cell of the last index path.
EDIT
Hard-coding the tap recognizer works
if ([message.type isEqualToString:#"image"]) {
UIImageView *view = ((ImageMessageCell *)cell).imageView;
UITapGestureRecognizer *tapRec = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(ui_tapped:)];
[view setGestureRecognizers:[NSArray arrayWithObject:tapRec]];
}
This is very tedious coding (since I need to check for each types of cells including image, video and other rich media), please post your answer if you know how to add it from the storyboard
It turns out that it's not able to add gesture recognizer to each instantiated prototype cell separately, i.e. only one gesture recognizer for all the cells instantiated from the same prototype cell.
Solved this problem by using UIButton with background image view instead of using image view.
[self.imageButton setBackgroundImage:[UIImage imageNamed:LOADING_IMAGE_FILE] forState:UIControlStateNormal];
NSData *blob = post.thumbnailBlob;
if (blob) {
[self.imageButton setBackgroundImage:[UIImage imageWithData:blob] forState:UIControlStateNormal];
}
In order for each cell to have it's unique tap gesture tag, it needs to be done in a proper sequence:
let tapGest = UITapGestureRecognizer(target: self, action: #selector(yourTapFunction(_:)))
//1 add gesture to the cell
cell.imageView.addGestureRecognizer(tapGest)
//2 assign a tag
tapGest.view?.tag = indexPath.row
#objc func yourTapFunction(_ tapGest: UITapGestureRecognizer) {
guard let index = tapGest.view?.tag else { return }
//do smth
}
if you reverse 1 and 2, the tag will always be 0

Resources