UICollectionViewCell上的长按手势

117

我想知道如何在(UICollectionView的子类中)添加长按手势识别。我在文档中读到默认已经添加了这个功能,但是我不知道具体怎么做。

我想要实现的是:

在单元格上进行长按操作(我从GitHub上下载了一个日历插件),获取被点击的单元格并对其执行某些操作。我需要知道哪个单元格被长按了。很抱歉我的问题比较笼统,但我在谷歌和StackOverflow上找不到更好的答案。

9个回答

231

Objective-C

在你的myCollectionViewController.h文件中,添加UIGestureRecognizerDelegate协议。

@interface myCollectionViewController : UICollectionViewController<UIGestureRecognizerDelegate>

在你的myCollectionViewController.m文件中:
- (void)viewDidLoad
{
    // attach long press gesture to collectionView
    UILongPressGestureRecognizer *lpgr 
       = [[UILongPressGestureRecognizer alloc]
                     initWithTarget:self action:@selector(handleLongPress:)];
    lpgr.delegate = self;
    lpgr.delaysTouchesBegan = YES;
    [self.collectionView addGestureRecognizer:lpgr];
}

-(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];
        // do stuff with the cell
    }
}

Swift

class Some {

    @objc func handleLongPress(gesture : UILongPressGestureRecognizer!) {
        if gesture.state != .Ended {
            return
        }
        let p = gesture.locationInView(self.collectionView)

        if let indexPath = self.collectionView.indexPathForItemAtPoint(p) {
            // get the cell at indexPath (the one you long pressed)
            let cell = self.collectionView.cellForItemAtIndexPath(indexPath)
            // do stuff with the cell
        } else {
            print("couldn't find index path")
        }
    }
}

let some = Some()
let lpgr = UILongPressGestureRecognizer(target: some, action: #selector(Some.handleLongPress))

Swift 4

class Some {

    @objc func handleLongPress(gesture : UILongPressGestureRecognizer!) {
        if gesture.state != .ended { 
            return 
        } 

        let p = gesture.location(in: self.collectionView) 

        if let indexPath = self.collectionView.indexPathForItem(at: p) { 
            // get the cell at indexPath (the one you long pressed) 
            let cell = self.collectionView.cellForItem(at: indexPath) 
            // do stuff with the cell 
        } else { 
            print("couldn't find index path") 
        }
    }
}

let some = Some()
let lpgr = UILongPressGestureRecognizer(target: some, action: #selector(Some.handleLongPress))

1
UICollectionViewCell* cell = [self.collectionView cellForItemAtIndexPath:indexPath]; 参考此处 希望这个回答能获得正确答案奖励 :D - abbood
11
至少在iOS7中,你需要添加lpgr.delaysTouchesBegan = YES;来避免首先触发didHighlightItemAtIndexPath - DynamicDan
7
为什么要添加 lpgr.delegate = self;?没有代理也能正常工作,而且你也没有提供代理。 - Yevhen Dubinin
3
答案可行,但在长按识别器处于活动状态时,我无法使用另一根手指在集合视图中上下滚动。发生了什么? - Pétur Ingi Egilsson
6
个人建议使用UIGestureRecognizerStateBegan,这样手势在被识别时就会被使用,而不是在用户释放手指时。 - Jeffrey Sun
显示剩余14条评论

31

相同的代码@abbood的Swift代码:

在viewDidLoad中:

let lpgr : UILongPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: "handleLongPress:")
lpgr.minimumPressDuration = 0.5
lpgr.delegate = self
lpgr.delaysTouchesBegan = true
self.collectionView?.addGestureRecognizer(lpgr)

以及这个函数:

func handleLongPress(gestureRecognizer : UILongPressGestureRecognizer){

    if (gestureRecognizer.state != UIGestureRecognizerState.Ended){
        return
    }

    let p = gestureRecognizer.locationInView(self.collectionView)

    if let indexPath : NSIndexPath = (self.collectionView?.indexPathForItemAtPoint(p))!{
        //do whatever you need to do
    }

}

不要忘记委托 UIGestureRecognizerDelegate


3
很好地解决了,只需注意将“handleLongPress:”更改为#selector(YourViewController.handleLongPress(_ :))即可。 - Joseph Geraghty
17
如果你希望代码在最短持续时间过去后触发而不仅仅是当用户抬起手指时触发,那么将UIGestureRecognizerState.Ended更改为UIGestureRecognizerState.Began即可。原始代码效果良好。 - Crashalot
当在集合视图的单元格之外的任何地方单击时,self.myCollectionView?.indexPathForItem(at: p)会返回致命错误:意外地发现nil值解包。 - Arjun
当您点击CollectionView并且没有单元格时,此方法会抛出“Fatal error: Unexpectedly found nil while unwrapping an Optional value”错误。如何解决这个错误? - Arjun

18

Swift 5:

->

Swift 5:

private func setupLongGestureRecognizerOnCollection() {
    let longPressedGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress(gestureRecognizer:)))
    longPressedGesture.minimumPressDuration = 0.5
    longPressedGesture.delegate = self
    longPressedGesture.delaysTouchesBegan = true
    collectionView?.addGestureRecognizer(longPressedGesture)
}

@objc func handleLongPress(gestureRecognizer: UILongPressGestureRecognizer) {
    if (gestureRecognizer.state != .began) {
        return
    }

    let p = gestureRecognizer.location(in: collectionView)

    if let indexPath = collectionView?.indexPathForItem(at: p) {
        print("Long press at item: \(indexPath.row)")
    }
}

还要别忘了实现UIGestureRecognizerDelegate并在viewDidLoad或需要调用它的任何地方调用setupLongGestureRecognizerOnCollection。


当您点击CollectionView并且没有单元格时,此方法会抛出“Fatal error: Unexpectedly found nil while unwrapping an Optional value”错误。如何解决这个错误? - Arjun
为什么在没有实现任何方法的情况下,你必须设置代理? - famfamfam

13

使用UICollectionView的代理来接收长按事件

你必须实现以下3个方法。

//UICollectionView menu delegate
- (BOOL)collectionView:(UICollectionView *)collectionView shouldShowMenuForItemAtIndexPath:(NSIndexPath *)indexPath{

   //Do something

   return YES;
}
- (BOOL)collectionView:(UICollectionView *)collectionView canPerformAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath withSender:(nullable id)sender{
    //do nothing
    return NO;
}

- (void)collectionView:(UICollectionView *)collectionView performAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath withSender:(nullable id)sender{
    //do nothing
}

注意:如果您在shouldShowMenuForItemAtIndexPath中返回false,则didSelectItemAtIndexPath也将被触发。当我想要长按和单击有两个不同的操作时,这对我造成了问题。 - ShannonS
现在这些方法已经被弃用,您可以使用 - (UIContextMenuConfiguration *)collectionView:(UICollectionView *)collectionView contextMenuConfigurationForItemAtIndexPath:(nonnull NSIndexPath *)indexPath point:(CGPoint)point;。 - Viktor Goltvyanitsa

8

这里提供的方法来添加自定义长按手势识别器是正确的,但是根据文档(链接):UICollectionView类的父类已安装了一个默认的长按手势识别器来处理滚动交互,因此您必须将自定义的点击手势识别器链接到与您的集合视图相关联的默认识别器。

以下代码将避免您的自定义手势识别器干扰默认手势识别器:

UILongPressGestureRecognizer* longPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPressGesture:)];

longPressGesture.minimumPressDuration = .5; //seconds
longPressGesture.delegate = self;

// Make the default gesture recognizer wait until the custom one fails.
for (UIGestureRecognizer* aRecognizer in [self.collectionView gestureRecognizers]) {
   if ([aRecognizer isKindOfClass:[UILongPressGestureRecognizer class]])
      [aRecognizer requireGestureRecognizerToFail:longPressGesture];
} 

我明白你的意思,但这并不是非黑即白的。文档中说:“UICollectionView类的父类安装了一个默认的轻拍手势识别器和一个默认的长按手势识别器来处理滚动交互。您永远不应该尝试重新配置这些默认手势识别器或用自己的版本替换它们。”因此,默认的长按识别器是为滚动而设计的...这意味着它必须伴随着垂直移动... OP并没有询问那种行为,也没有试图替换它。 - abbood
抱歉听起来有点防御,但我已经在我的iOS应用程序中使用了上述代码数月了...无法想象会出现任何故障。 - abbood
1
@abbood 没问题。我不知道 PO 正在使用哪个第三方日历组件,但是即使您认为它永远不会发生故障,防止故障也不是一个坏主意;-) - tiguero
如果你必须等待默认识别器失败,那么这是否意味着会有延迟? - Crashalot

2
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPress:)];

[cell addGestureRecognizer:longPress];

并添加以下方法。
- (void)longPress:(UILongPressGestureRecognizer*)gesture
{
    if ( gesture.state == UIGestureRecognizerStateEnded ) {

        UICollectionViewCell *cellLongPressed = (UICollectionViewCell *) gesture.view;
    }
}

1
也许,使用UILongPressGestureRecognizer是最常见的解决方案。但我遇到了两个烦人的问题:
  • 有时当我们移动触摸时,这个识别器会以错误的方式工作;
  • 识别器拦截其他触摸操作,因此我们不能以正确的方式使用我们的UICollectionView的高亮回调。
让我提出一个有点暴力但按要求工作的建议:
声明一个回调描述,用于长时间点击我们的单元格: typealias OnLongClickListener = (view:OurCellView) -> VoidUICollectionViewCell扩展为变量(例如,我们可以将其命名为OurCellView):
/// To catch long click events.
private var longClickListener: OnLongClickListener?

/// To check if we are holding button pressed long enough.
var longClickTimer: NSTimer?

/// Time duration to trigger long click listener.
private let longClickTriggerDuration = 0.5

在我们的单元格类中添加两个方法:
/**
 Sets optional callback to notify about long click.

 - Parameter listener: A callback itself.
 */
func setOnLongClickListener(listener: OnLongClickListener) {
    self.longClickListener = listener
}

/**
 Getting here when long click timer finishs normally.
 */
@objc func longClickPerformed() {
    self.longClickListener?(view: self)
}

在这里覆盖触摸事件:

/// Intercepts touch began action.
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
    longClickTimer = NSTimer.scheduledTimerWithTimeInterval(self.longClickTriggerDuration, target: self, selector: #selector(longClickPerformed), userInfo: nil, repeats: false)
    super.touchesBegan(touches, withEvent: event)
}

/// Intercepts touch ended action.
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
    longClickTimer?.invalidate()
    super.touchesEnded(touches, withEvent: event)
}

/// Intercepts touch moved action.
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
    longClickTimer?.invalidate()
    super.touchesMoved(touches, withEvent: event)
}

/// Intercepts touch cancelled action.
override func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) {
    longClickTimer?.invalidate()
    super.touchesCancelled(touches, withEvent: event)
}

然后在我们的集合视图控制器中声明回调监听器:

let longClickListener: OnLongClickListener = {view in
    print("Long click was performed!")
}

最后在 cellForItemAtIndexPath 中为单元格设置回调函数:

/// Data population.
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath)
    let castedCell = cell as? OurCellView
    castedCell?.setOnLongClickListener(longClickListener)

    return cell
}

现在我们可以拦截单元格的长按操作。

1
为了在UICollectionView上拥有一个外部手势识别器并且不与内部手势识别器冲突,您需要执行以下操作:
添加您的手势识别器,设置它并在某处捕获对它的引用(如果您子类化了UICollectionView,则最好的选项是在您的子类中)
@interface UICollectionViewSubclass : UICollectionView <UIGestureRecognizerDelegate>    

@property (strong, nonatomic, readonly) UILongPressGestureRecognizer *longPressGestureRecognizer;   

@end

重写默认的初始化方法initWithFrame:collectionViewLayout:initWithCoder:,并为您的长按手势识别器添加设置方法

@implementation UICollectionViewSubclass

-(instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout
{
    if (self = [super initWithFrame:frame collectionViewLayout:layout]) {
        [self setupLongPressGestureRecognizer];
    }
    return self;
}

-(instancetype)initWithCoder:(NSCoder *)aDecoder
{
    if (self = [super initWithCoder:aDecoder]) {
        [self setupLongPressGestureRecognizer];
    }
    return self;
}

@end

编写设置方法,使其实例化长按手势识别器,设置其委托,使用UICollectionView手势识别器设置依赖关系(使其成为主要手势,所有其他手势在该手势失败之前等待被识别),并将手势添加到视图中。

-(void)setupLongPressGestureRecognizer
{
    _longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self
                                                                                action:@selector(handleLongPressGesture:)];
    _longPressGestureRecognizer.delegate = self;

    for (UIGestureRecognizer *gestureRecognizer in self.collectionView.gestureRecognizers) {
        if ([gestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]]) {
            [gestureRecognizer requireGestureRecognizerToFail:_longPressGestureRecognizer];
        }
    }

    [self.collectionView addGestureRecognizer:_longPressGestureRecognizer];
}

同时不要忘记实现UIGestureRecognizerDelegate方法,以使手势失败并允许同时识别(您可能需要或者不需要实现它,这取决于您拥有的其他手势识别器或依赖于内部手势识别器)。

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
    if ([self.longPressGestureRecognizer isEqual:gestureRecognizer]) {
        return NO;
    }

    return NO;
}

凭证用于内部实现 LXReorderableCollectionViewFlowLayout


这是我为我的 Snapchat 克隆所做的相同舞蹈。啊,真爱。 - user2849654

1

更简单的解决方案。

在你的cellForItemAt代理方法中(设置.tag属性以备后用):

cell.gestureRecognizers?.removeAll()
cell.tag = indexPath.row
let directFullPreviewer = UILongPressGestureRecognizer(target: self, action: #selector(directFullPreviewLongPressAction))
cell.addGestureRecognizer(directFullPreviewer)

并为长按添加回调函数:

@objc func directFullPreviewLongPressAction(g: UILongPressGestureRecognizer)
{
    if g.state == UIGestureRecognizer.State.began
    {
        // Get index as g.view.tag and that's it
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接