I've set up a UITapGestureRecognizer for a UIScrollView inside a UICollectionView. I've configured it to properly detect taps and trigger a method I wrote, but if I try to set the selector to collectionView:didSelectItemAtIndexPath: the program crashes when a cell is tapped.
Any idea why this is the case?
This works:
UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tapped:)];
- (void) tapped:(UIGestureRecognizer *)gesture{
//some code
}
This does not work:
UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(collectionView:didSelectItemAtIndexPath:)];
- (void) collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
//some code
}
the code you wrote,
UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(collectionView:didSelectItemAtIndexPath:)];
The selector is generally just a singleFunction with one input argument which is UITapGestureRecogniser object.
should be like this,
-(void)clicked:(UIGestureRecogniser *)ges{
}
But the selector you used it improper, because it needs two inputs which cant be supplied with gestureRecogniser.Hence the crash.
Change your above code to below one,
UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(clicked:)];
-(void)clicked:(UIgestureRecogniser *)ges{
//use gesture to get get the indexPath, using CGPoint (locationInView).
NSIndexPath *indexPath = ...;
[self collectionView:self.collectionView didSelectItemAtIndexPath:indexPath];
}
The action for a gesture recognizer must conform to one of the following signatures:
- (void)handleGesture;
- (void)handleGesture:(UIGestureRecognizer *)gestureRecognizer;
You need to use one of these action signatures and do whatever you need to in that method
, including determining the correct indexPath for the gesture.
See the docs:
https://developer.apple.com/library/ios/documentation/uikit/reference/UIGestureRecognizer_Class/Reference/Reference.html#//apple_ref/occ/instm/UIGestureRecognizer/initWithTarget:action:
We have to call the didSelectItemAtIndexPath from the proper reference object.
UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tapped:)];
- (void) tapped:(UIGestureRecognizer *)gesture{
NSIndexPath *indexPath = //create your custom index path here
[self.collectionViewObject didSelectItemAtIndexPath:indexPath];
}
Related
I'm creating UICollectionView like below.
What I want to do about the UICollectionView is,
Each UICollectionViewCell has UIButton ([cell.contentView addSubview:button];)
UIButton need to handle UIControlEventTouchDown and UIControlEventTouchUpInside
Begin interactiveMovement by Long tap of UICollectionView Cell
Don't want to begin interactiveMovement by Long tap of UIButton. UIButton just handle UIControlEvent assigned at Step 2.
Want to handle (didSelectItemAtIndexPath:) when the area of UICollectionViewCell other than UIButton is tapped.
My problem is that handleLongPress for UICollectionView responds and start interactiveMovement even when long tapping UIButton .
I want to start interactiveMovement only when the area of UICollectionView other than UIButton is tapped.
When UIButton is long tapped, I just want to handle UIConrolEvent assigned to UIButton and don't want UICollectionView to respond.
I'd appreciate if you would have some solution.
My code is like below, thank you.
MyCollectionView : UICollectionView
- (id)init
{
self = [super initWithFrame:CGRectZero collectionViewLayout:[[UICollectionViewFlowLayout alloc] init]];
if (self) {
self.delegate = self;
self.dataSource = self;
--- snip ---
[self addLongPressGesture];
--- snip ---
}
return self;
}
- (void)addLongPressGesture
{
UILongPressGestureRecognizer *gesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self
action:#selector(handleLongPress:)];
[self addGestureRecognizer:gesture];
}
- (void)handleLongPress:(UILongPressGestureRecognizer *)sender
{
CGPoint point = [sender locationInView:sender.view];
NSIndexPath *indexPath = [self indexPathForItemAtPoint:point];
switch (sender.state) {
case UIGestureRecognizerStateBegan:
[self beginInteractiveMovementForItemAtIndexPath:indexPath];
break;
case UIGestureRecognizerStateChanged:
[self updateInteractiveMovementTargetPosition:point];
break;
case UIGestureRecognizerStateEnded:
[self endInteractiveMovement];
break;
default:
[self cancelInteractiveMovement];
break;
}
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
// something to do
}
MyUICollectionViewCell : UICollectionViewCell
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self initButton];
}
return self;
}
- (void)initButton
{
_button = [[UIButton alloc] init];
[_button addTarget:self action:#selector(touchUpButton:) forControlEvents:UIControlEventTouchUpInside];
[_button addTarget:self action:#selector(touchDownButton:) forControlEvents:UIControlEventTouchDown];
--- snip ---
[self addSubview:_button];
}
- (void)touchUpButton:(UIButton *)button
{
// something to do
}
- (void)touchDownButton:(UIButton *)button
{
// something to do
}
Add a transparent UIView below UIButton and add long press to it not to the whole collection view as UIButton is considered a sub-Control of the cell
OR
see the long press point x,y if it's intersects with button's frame return from the method
I've resolved my problem. It my not be a good solution but it's working as I expect.
I've added ButtonBaseView(:UIView) under UIButton and add LongPressGesture on the ButtonBaseView.
The LongPressGesture do nothing as it's action. The role is to block LongPressGesture not to pass the event to UICollectionView.
At the same time, set cancelsTouchesInView=NO to handle TouchUpInside or TouchDragExit
My code is like below,
- (void)addDummyLongTapGesture
{
UILongPressGestureRecognizer *gesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:nil];
gesture.cancelsTouchesInView = NO;
[buttonBaseView addGestureRecognizer:gesture];
}
In my app I have UITableView cells with multiple buttons. 4 UIButtons per cell. For each UIButton I want to add a UILongPressGestureRecognizer. Code below:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
FriendViewCell *cell = (FriendViewCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
for(int i =0; i<4; i++) {
UIButton *button = cell.buttons[i];
UILabel *label = cell.labels[i];
[button setTag:(int)indexPath.row*4+i];
[button addTarget:self action:#selector(friendTapped:) forControlEvents:UIControlEventTouchUpInside];
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc]initWithTarget:self action:#selector(longPressFriend:)];
[button addGestureRecognizer:longPress];
}
}
I just realised though that if a cell is reused then I am adding a gesture multiple times per button. Is there a way to detect if the cell created is reused or new? I don't think I can move the code to the FriendViewCell class because my gesture target friendTapped: is in my UITableViewController. Any pointers will be greatly appreciated! thanks
A cleaner way would be to create a custom class for your Button. This class would have an UILongPressGestureRecognizer created at initialization and a delegate (your controller) that will be called when the gesture is triggered.
.h
#class MyLongPressedButton
#protocol MyLongPressedButtonDelegate
- (void)buttonIsLongPressed:(MyLongPressedButton *)button;
#end
#interface MyLongPressedButton
#property (nonatomic, weak) id<MyLongPressedButtonDelegate > delegate
#end
.m
-(id)init {
if (self = [super init]) {
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc]initWithTarget:self action:#selector(longPress:)];
[self addGestureRecognizer:longPress];
}
return self;
}
-(void)longPress:(id)sender {
[_delegate buttonIsLongPressed:self]
}
First of all, there are fast ways to solve this - you can check the existing gesture recognizers on the button whether there is a long press gesture recognizer.
A slightly better solution is to define a property on the cell, e.g.
#property (nonatomic, assign) BOOL recognizerAdded
However, the best solution is to do this inside the cell class
#implementation FriendViewCell
- (void)awakeFromNib {
//add the recognizers
}
#end
Note that you your table delegate shouldn't care about the structure of your cell, it's the cell's responsibility to setup itself properly.
Of course, you will need a delegate on the cell to notify you about the action but it will be a cleaner solution.
I would recommend placing the code inside your cell and creating a delegate method that will return a reference to the cell.
E.g.
#protocol myAwesomebuttonCellDelegate <NSObject>
- (void)myAwesomeCell:(MyAwesomeCell *)cell buttonPressed:(UIButton *)btn;
#end
Then in your view controller you can use:
NSIndexPath *index = [tblView indexPathForCell:cell];
to get the row / section
I'm using UITapGestureRecognizer to end editing (because that's the workaround I found useful to end editing on input keyboards that have no other way to do so, like the Decimal Pad). The problem is that inside that viewController I have a tableView (the UITableViewDataSource and UITableViewDelegate of that tableView are set to the viewController) and the method didSelectRowAtIndexPath is not being triggered.
Code:
viewDidLoad {
...
UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tap:)];
[self.view addGestureRecognizer:tapRecognizer];
...
}
- (void)tap:(UIGestureRecognizer*)gr {
[self.view endEditing:YES];
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSLog(#"Help!");
}
I know the UITapGestureRecognizer is catching the selection, because if I comment out as following:
viewDidLoad {
...
UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tap:)];
//[self.view addGestureRecognizer:tapRecognizer];
...
}
now the method the didSelectRowAtIndexPath finally triggers out.
I need help with some good practices on how to workaround the "endEditing" or how to forward the tap gesture to the tableView so that the didSelectRowAtIndexPath triggers out.
Thanks!
I assume that you haven't a tableView that takes all the screen, otherwise you can close the keyboard in didSelectRowAtIndexPath.
Any way, you can do your stuff and the pass the event to the next responder (that is kind of class UITableViewCell if there is, so if user tap on the tableView):
- (void)tap:(UIGestureRecognizer*)gr {
[self.view endEditing:YES];
UIResponder *responder = self;
while (responder.nextResponder != nil){
responder = responder.nextResponder;
if ([responder isKindOfClass:[UITableViewCell class]]) {
break;
}
}
[responder touchesBegan:touches withEvent:event];
}
I have a custom complex UITableViewCell where there are many views. I have an UIImageView within it which is visible for a specific condition. When it's visible ,
I have to perform some Action when user Taps that UIImageView.
I know I have to trigger a selector for this task. But I also want to pass a value to that Method (please See -(void)onTapContactAdd :(id) sender : (NSString*) uid below) that will be called as a Action of Tap on my UIImageView in UITableViewCell I am talking about. It's because , using that passed value , the called method will do it's job.
Here is what I have tried so far.
cell.AddContactImage.hidden = NO ;
cell.imageView.userInteractionEnabled = YES;
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(onTapContactAdd::)];
[tap setNumberOfTouchesRequired:1];
[tap setNumberOfTapsRequired:1];
[tap setDelegate:self];
[cell.AddContactImage addGestureRecognizer:tap];
-(void)onTapContactAdd :(id) sender : (NSString*) uid
{
NSLog(#"Tapped");
// Do something with uid from parameter
}
This method is not called when I tap. I have added in my header file.
Thanks for your help in advance.
Maybe not the ideal solution, but add tags to each of the UIImageViews. Then have an NSArray with the uid's corresponding to the tag values
So somewhere in your code make the array
NSArray *testArray = [NSArray arrayWithObjects:#"uid1", #"uid2", #"uid3", #"uid4", #"uid5", #"uid6", nil];
Then when you're setting up the tableview cells set the tag to the row #
//Set the tag of the imageview to be equal to the row number
cell.imageView.tag = indexPath.row;
//Sets up taprecognizer for each imageview
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self
action:#selector(handleTap:)];
[cell.imageView addGestureRecognizer:tap];
//Enable the image to be clicked
cell.imageView.userInteractionEnabled = YES;
Then in the method that gets called you can get the tag like this
- (void)handleTap:(UITapGestureRecognizer *)recognizer
{
NSString *uid = testArray[recognizer.view.tag];
}
Add the gesture recognizer to the cell itself.
Then in the action selector, do as following to know which view has been tapped:
-(IBAction)cellTapped:(UITapGestureRecognizer*)tap
{
MyCustomTableViewCell* cell = tap.view;
CGPoint point = [tap locationInView:cell.contentView];
UIView* tappedView = [cell.contentView hitTest:point withEvent:NULL];
if (tappedView==cell.myImageView) {
// Do whatever you want here,
}
else { } // maybe you have to handle some other views here
}
A gesture recognizer will only pass one argument into an action selector: itself. So u need to pass the uid value alone.Like this.
Guessing this lies within the cellForRowAtIndexPath: method
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
//code
cell.AddContactImage.hidden = NO ;
cell.imageView.userInteractionEnabled = YES;
cell_Index=indexPath.row ;
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(onTapContactAdd:)]; //just one arguement passed
[tap setNumberOfTouchesRequired:1];
[tap setNumberOfTapsRequired:1];
[tap setDelegate:self];
[cell.AddContactImage addGestureRecognizer:tap];
//rest of code
}
-(void)onTapContactAdd :(NSString*) uid
{
NSLog(#"Tapped");
CustomCell *cell=(CustomCell *)[yourtableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:cell_Index inSection:0]];
//cell.AddContactImage will give you the respective image .
// Do something with uid from parameter .
}
So here when you tap on the visible image in the respective custom cell,the onTapContactAdd: method gets called with the corresponding uid value(parameter) and now we have the cell.AddContactImage also accessible which i believe is why you were trying to pass it also along with the parameters .
Hope it Helps!!!
you can use ALActionBlocks to add gesture to UIImageView and handle action in block
__weak ALViewController *wSelf = self;
imageView.userInteractionEnabled = YES;
UITapGestureRecognizer *gr = [[UITapGestureRecognizer alloc] initWithBlock:^(UITapGestureRecognizer *weakGR) {
NSLog(#"pan %#", NSStringFromCGPoint([weakGR locationInView:wSelf.view]));
}];
[self.imageView addGestureRecognizer:gr];
Install
pod 'ALActionBlocks'
Another one, with the indexPath, if it's ok for you to handle the tap in DataSource:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseId)! as! ...ListCell
...
cell.theImage.isUserInteractionEnabled = true
let onTap = UITapGestureRecognizer(target: self, action: #selector(onTapImage))
onTap.numberOfTouchesRequired = 1
onTap.numberOfTapsRequired = 1
cell.theImage.addGestureRecognizer(onTap)
...
return cell
}
func onTapImage(_ sender: UITapGestureRecognizer) {
var cell: ...ListCell?
var tableView: UITableView?
var view = sender.view
while view != nil {
if view is ...ListCell {
cell = view as? ...ListCell
}
if view is UITableView {
tableView = view as? UITableView
}
view = view?.superview
}
if let indexPath = (cell != nil) ? tableView?.indexPath(for: cell!) : nil {
// this is it
}
}
You may want to make the code shorter if you have only one tableView.
Here, We have customtableviewcell both .h and .m files with two images in cell. And HomeController, which have tableview to access this cell. This is detect the Tap on both the UIImage as described.
**CustomTableViewCell.h**
#protocol CustomTableViewCellDelegate <NSObject>
- (void)didTapFirstImageAtIndex:(NSInteger)index;
-(void)didTapSettingsImagAtIndex:(NSInteger)index;
#end
#interface CustomTableViewCell : UITableViewCell
{
UITapGestureRecognizer *tapGesture;
UITapGestureRecognizer *tapSettingsGesture;
}
#property (weak, nonatomic) IBOutlet UIImageView *firstImage;
#property (weak, nonatomic) IBOutlet UIImageView *lightSettings;
#property (nonatomic, assign) NSInteger cellIndex;
#property (nonatomic, strong) id<CustomTableViewCellDelegate>delegate;
**CustomTableViewCell.m**
#import "CustomTableViewCell.h"
#implementation CustomTableViewCell
- (void)awakeFromNib {
[super awakeFromNib];
// Initialization code
[self addGestures];
}
- (void)addGestures {
tapGesture = [[UITapGestureRecognizer alloc]
initWithTarget:self action:#selector(didTapFirstImage:)];
tapGesture.numberOfTapsRequired = 1;
[_firstImage addGestureRecognizer:tapGesture];
tapSettingsGesture = [[UITapGestureRecognizer alloc]
initWithTarget:self action:#selector(didTapSettingsImage:)];
tapSettingsGesture.numberOfTapsRequired = 1;
[_lightSettings
addGestureRecognizer:tapSettingsGesture];
}
- (void)didTapFirstImage:(UITapGestureRecognizer *)gesture
{
if (_delegate) {
[_delegate didTapFirstImageAtIndex:_cellIndex];
}
}
-(void)didTapSettingsImage: (UITapGestureRecognizer
*)gesture {
if(_delegate) {
[_delegate didTapSettingsAtIndex:_cellIndex];
}
}
**HomeController.h**
#import <UIKit/UIKit.h>
#import "CustomTableViewCell.h"
#interface HomeController : CustomNavigationBarViewController
<UITableViewDelegate, UITableViewDataSource,
UIGestureRecognizerDelegate, CustomTableViewCellDelegate>
#end
**HomeController.m**
---------------------
#import "HomeController.h"
#import "CustomTableViewCell.h"
#implementation HomeController
-(NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section {
return 2 (Number of rows) ;
// return number of rows
}
-(UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
CustomTableViewCell *cell = [tableView
dequeueReusableCellWithIdentifier:#"cell"
forIndexPath:indexPath];
cell.delegate = self;
cell.cellIndex = indexPath.row;
cell.firstImage.userInteractionEnabled = YES;
cell.lightSettings.userInteractionEnabled = YES;
return cell;
}
-(void)didTapFirstImageAtIndex:(NSInteger)index
{
NSLog(#"Index %ld ", (long)index);
//Do whatever you want here
}
-(void)didTapSettingsAtIndex:(NSInteger)index
{
NSLog(#"Settings index %ld", (long)index);
// Do whatever you want here with image interaction
}
#end
I have strange issue - when I register TapGestureRecognizer in the cellForRowAtIndexPath method it works perfect, but when I register TapGestureRecognizer in cell's initWithStyle method tap recognition doesn't work, breakpoint doesn't hit in handler.
The following works.
I have created custom table view cell with corresponding xib file and registered it.
[self.tableView registerNib:[UINib nibWithNibName:#"MyCell"
bundle:[NSBundle mainBundle]]
forCellReuseIdentifier:#"cell"];
...
and in the cellForRowAtIndexPath
MyCell *cell = [tableView dequeueReusableCellWithIdentifier:#"cell"];
...
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self
action:#selector(didTapCell:)];
[tap setNumberOfTapsRequired:1];
[cell addGestureRecognizer:tap];
The following doesn't work
#implementation MyCell
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
UITapGestureRecognizer *tgr = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleCellTap:)];
[tgr setDelegate:self];
[tgr setNumberOfTapsRequired:1];
[tgr setNumberOfTouchesRequired:1];
[self addGestureRecognizer:tgr];
//[self.contentView addGestureRecognizer:tgr]; also doesn't work
}
return self;
}
I can leave the working solution, but I want to move the gesture recognition to cell initialization and fire tap event through my delegate.
Why is tap recognition not working if I'm registering recognizer in the cell initialization?
Are you sure initWithStyle:reuseIdentifier is called? Afaik you have to use initWithCoder: if you register a nib for the cell.
In a project of mine I have this
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self) {
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePan:)];
pan.delegate = self;
self.gestureRecognizers = [NSArray arrayWithObject:pan];
}
return self;
}
So I am using a pan gesture recognizer and it works from within the init method.
You have registered a xib for a particular cell identifier. Now the tableview will automatically instantiate a cell for you if needed (when you call dequeReusableCell...) but the initWithStyle:reuseIdentifier method does not get called, so your gesture recognizer is never created/added.
If you do need to 'init' stuff when using registered xib(s), override the awakeFromNib in your custom cell class and put your code there. I usually put my 'init' code in a separate method and call it from both initWithStyle and awakeFromNib overrides.