1

I have a simple situation. UICollectionView and custom UICollectionViewCell. The UICollectionView has a navigation bar. The custom cell has an image. I want to push a second controller to the navigation stack when the image is clicked. I added tap gestures to the image inside the custom view. How can I access the navigation bar in the parent controller from the cell? Or should I be adding the tap click gesture to the UICollectionView and not the UICollectionViewCell cell?

self.templateImage.userInteractionEnabled = YES;
self.userInteractionEnabled = YES;
UITapGestureRecognizer *tapGestureRecog = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleSelect:)];
[self.templateImage addGestureRecognizer:tapGestureRecog];
[self addGestureRecognizer:tapGestureRecog];

templateImage is the image inside the cell. This code is called inside the initWithCoder method of the UICollectionViewCell. So I guess my question is how to access the controller from the view. Usually this would be done with an IBAction. Newbie to iOS :)

Tyler Cloutier
  • 2,040
  • 2
  • 21
  • 31
U-L
  • 2,671
  • 8
  • 35
  • 50

1 Answers1

1

As mentioned in other answers, you can only attach a gesture recognizer to one view.

You could always have the UICollectionView add the gesture recognizer to the collection view cell in cellForItemAtIndexPath:

-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
    CustomCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kCustomCollectionViewCellIdentifier forIndexPath:indexPath];
    if (cell.contentView.gestureRecognizers.count == 0) {
        [cell.contentView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(myViewWasTapped:)]];
    }

    return cell;
}

If you go with this approach, you will have to make sure that you do not continuously add gesture recognizers to the same cell if the cell is reused. You can check the view's gestureRecognizers property to see if the cell's contentView already has a gesture recognizer.

When the cell is tapped, you can then access the view that was tapped through the view property of the gestureRecognizer passed in to your method myViewWasTapped:(UIGestureRecognizer *)sender.

Another way to do this is with delegation, a popular method of passing information around in iOS. If you subclass UICollectionViewCell, you can declare a protocol for the "delegate" of the cell. This is, basically, just a list of all the methods that an object should implement if it wants to be the delegate of the cell. In you UICollectionViewCell h file, you can declare the protocol in the following way.

@class CustomCardCollectionViewCell;

@protocol CustomCollectionViewCellDelegate <NSObject>
@required
- (void)customCollectionViewCellWasTapped:(CustomCollectionViewCell *)cell;
@end

Then in your cell's interface you would declare a property called delegate (although you can call it whatever you want). Delegate is of type id which conforms to the protocol CustomCollectionViewCellDelegate or whatever you decide to call your protocol.

@interface CustomCollectionViewCell : UICollectionViewCell
    @property (weak, nonatomic) id<CustomCollectionViewCellDelegate> delegate;

    //Other interface declaration stuffs.
@end

Importantly, the UICollectionView will have to set the delegate property of the cell to be itself when it creates the cell. You can think of this as a back pointer (which is why the delegate property is declared weak) to the UICollectionView from the CustomCollectionViewCell.

-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
    CustomCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kCustomCollectionViewCellIdentifier forIndexPath:indexPath];
    cell.delegate = self;

    return cell;
}

The benefit of delegation is that the delegate doesn't have to be any particular type of object, except one that conforms to the protocol that you defined. Therefore, you don't need the custom cell to be aware of any other classes. Hooray!

You also need to remember to have the cell call the method on it's delegate when the cell's gesture recognizer detects a tap.

[self.delegate customCollectionViewCellWasTapped:self];

FOLLOW UP QUESTION

If you need to detect that a particular subview of the cell was tapped, there are a few ways of solving that problem. One way would be to expose the subview as a public property (readonly perhaps) on your subclass of UICollectionViewCell and attach the UIGestureRecognizer directly to that subview so that it would only fire when the subview was tapped. Let's say that subview is a star button indicating a favorite. This could then be accomplished in the following way.

-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
    CustomCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kCustomCollectionViewCellIdentifier forIndexPath:indexPath];
    if (cell.contentView.gestureRecognizers.count == 0) {
        [cell.starView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(starWasTapped:)]];
    }

    return cell;
}

You could, alternatively, add a delegate method to your delegate protocol. Something like

- (void)customCollectionCell:(CustomCollectionViewCell *)cell starViewWasTapped:(UIView *)starView

If you go with this method, you could either have the CustomCollectionViewCell attach it's gesture recognizer directly to the star view, or attach the gesture recognizer to the contentView of the cell and calculate whether the gesture recognizer's locationInView falls within the frame of the starView.

You should avoid adding the gesture recognizer to the content view and having the UICollectionView calculate the location of the touch within the cell to determine if the star was tapped. The reason is that this would require the UICollectionView to have knowledge of the location of the star within the contentView of the cell. Each view should be responsible for the location of it's subviews.

If the star was a UIButton, you could forgo the UIGestureRecognizer entirely by setting the tag property on the button and having the UICollectionView add it's self as a target on the button. You can then tell which button in which cell was tapped by checking the tag property of the button that is sent with the action.

-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
    CustomCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kCustomCollectionViewCellIdentifier forIndexPath:indexPath];
    [cell.starButton addTarget:self action:@selector(starButtonWasTapped:) forControlEvents:UIControlEventTouchUpInside];
    cell.starButton.tag = indexPath.row;

    return cell;
}

- (void)starButtonWasTapped:(UIButton *)starButton {
    //Do something based off of the tag of the button.
}
Community
  • 1
  • 1
Tyler Cloutier
  • 2,040
  • 2
  • 21
  • 31
  • 1
    Nice answer! I'd add a condition to see if there is an actual `delegate` and if it has implemented the method, just to avoid a crash. – jbouaziz Feb 15 '14 at 06:11
  • Checking to see if there is a delegate is not necessary since sending a message to nil does nothing in Objective C, and so it won't cause a crash. It might be good to check that the object conforms to the selector, but the method was declared `@required` so you may want an error to be thrown if the delegate does not in fact implement the method which is declared as required. – Tyler Cloutier Feb 15 '14 at 06:34
  • Thank you @TylerCloutier. Further question: if the use case was to recognize tap for a particular subview in the cell. For example, if there is a star button in the cell that marks the cell as a favorite, I would need to determine if the star was pressed or something else in the cell. Would one solution be to get the location of the tap from the recognizer and then see if it falls in the frame/bounds of the star. If it does, the star was tapped. In my case, the image takes up all of the cell, so your previous answer works. But I am generalizing for the sake of learning. Thank you ! – U-L Feb 15 '14 at 17:47
  • @U-L This is a good question. Since there are a few different ways of solving that problem I edited my answer so I could better it. I hope that is what you were looking for. :D – Tyler Cloutier Feb 15 '14 at 18:32