Gesto de pressão longa em UICollectionViewCell

108

Eu queria saber como adicionar um reconhecedor de gestos de pressionar longamente a uma (subclasse de) UICollectionView. Eu li na documentação que ele é adicionado por padrão, mas não consigo descobrir como.

O que eu quero fazer é: Pressione e segure em uma célula ( eu tenho uma coisa de calendário do github ), pegue qual célula está tocada e faça outras coisas com ela. Preciso saber qual célula está comprimida. Desculpe por esta pergunta ampla, mas não consegui encontrar nada melhor no google ou no SO

Oscar Apeland
fonte

Respostas:

220

Objective-C

Em seu myCollectionViewController.harquivo, adicione o UIGestureRecognizerDelegateprotocolo

@interface myCollectionViewController : UICollectionViewController<UIGestureRecognizerDelegate>

em seu myCollectionViewController.marquivo:

- (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
    }
}

Rápido

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))
abbood
fonte
1
já está na resposta: UICollectionViewCell* cell = [self.collectionView cellForItemAtIndexPath:indexPath];referência aqui espero que tudo isso mereça uma resposta correta Prêmio: D
abbood
10
Para (pelo menos) ios7, você deve adicionar lpgr.delaysTouchesBegan = YES;para evitar que didHighlightItemAtIndexPathseja disparado primeiro.
DynamicDan
7
Por que você adicionou lpgr.delegate = self;? Ele funciona bem sem delegado, que você também não forneceu.
Yevhen Dubinin
3
@abbood, a resposta funciona, mas não consigo rolar para cima e para baixo na visualização da coleção (usando outro dedo) enquanto o reconhecedor de pressionamento longo está ativo. O que da?
Pétur Ingi Egilsson
4
Pessoalmente, eu faria UIGestureRecognizerStateBegan, para que o gesto seja usado quando for reconhecido, não quando o usuário soltar o dedo.
Jeffrey Sun,
28

O mesmo código @ código de abbood para Swift:

Em viewDidLoad:

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

E a função:

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
    }

}

Não se esqueça do delegado UIGestureRecognizerDelegate

Guilherme de Freitas
fonte
3
Funcionou muito bem, apenas uma observação de que "handleLongPress:" deve ser alterado para #selector (YourViewController.handleLongPress (_ :))
Joseph Geraghty
16
Funciona bem, mas mude UIGestureRecognizerState.Endedpara UIGestureRecognizerState.Beganse quiser que o código seja disparado depois que a duração mínima tiver passado, não apenas quando o usuário pegar o dedo.
Crashalot
11

Use o delegado de UICollectionView para receber evento de imprensa longa

Você deve implantar o 3 método abaixo.

//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
}
liuyuning
fonte
Observação: se você retornar false para shouldShowMenuForItemAtIndexPath, o didSelectItemAtIndexPath também será iniciado. Isso se tornou um problema para mim quando eu queria duas ações diferentes para pressionamento longo versus pressionamento único.
ShannonS
são métodos obsoletos por enquanto, você pode usar - (UIContextMenuConfiguration *) collectionView: (UICollectionView *) collectionView contextMenuConfigurationForItemAtIndexPath: (não nulo NSIndexPath *) indexPath ponto: (CGPoint) ponto;
Viktor Goltvyanitsa
8

As respostas aqui para adicionar um reconhecedor de gestos de toque longo personalizado estão corretas, no entanto, de acordo com a documentação aqui : a classe pai da UICollectionViewclasse instala um default long-press gesture recognizerpara lidar com as interações de rolagem, portanto, você deve vincular seu reconhecedor de gestos de toque personalizado ao reconhecedor padrão associado à sua visualização de coleção.

O código a seguir evitará que o reconhecedor de gestos personalizado interfira com o padrão:

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];
} 
tiguero
fonte
vejo o que você está dizendo, mas não é preto e branco, diz a documentação: The parent class of UICollectionView class installs a default tap gesture recognizer and a default long-press gesture recognizer to handle scrolling interactions. You should never try to reconfigure these default gesture recognizers or replace them with your own versions.então o reconhecedor de pressionamento longo padrão é feito para rolagem .. o que implica que deve ser acompanhado por um movimento vertical .. o OP não está perguntando sobre esse tipo de comportamento e ele não está tentando substituí-lo
abbood,
desculpe por soar na defensiva, mas estou usando o código acima com meu aplicativo iOS por meses. Não consigo me lembrar de uma única vez que uma falha aconteceu
abbood
@abbood, tudo bem. Não sei o componente de calendário de terceiros que o pedido de compra está usando, mas acho que evitar uma falha, mesmo que você ache que isso nunca pode acontecer, não pode ser uma má ideia ;-)
tiguero
Se você deve esperar que o reconhecedor padrão falhe, isso não significa que haverá um atraso?
Crashalot
2
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPress:)];

[cell addGestureRecognizer:longPress];

e adicione o método como este.

- (void)longPress:(UILongPressGestureRecognizer*)gesture
{
    if ( gesture.state == UIGestureRecognizerStateEnded ) {

        UICollectionViewCell *cellLongPressed = (UICollectionViewCell *) gesture.view;
    }
}
Satheeshwaran
fonte
2

Para ter um reconhecedor de gestos externo e não entrar em conflito com os reconhecedores de gestos internos no UICollectionView, você precisa:

Adicione seu reconhecedor de gestos, configure-o e capture uma referência para ele em algum lugar (a melhor opção está em sua subclasse se você criou uma subclasse de UICollectionView)

@interface UICollectionViewSubclass : UICollectionView <UIGestureRecognizerDelegate>    

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

@end

Substituir métodos de inicialização padrão initWithFrame:collectionViewLayout:e initWithCoder:e adicionar configurar método para você pressão longa gesto reconhecedor

@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

Escreva seu método de configuração para instanciar o reconhecedor de gestos de toque longo, definir seu delegado, configurar dependências com o reconhecedor de gestos UICollectionView (para que seja o gesto principal e todos os outros gestos esperarão até que o gesto falhe antes de serem reconhecidos) e adicione o gesto à visualização

-(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];
}

Também não se esqueça de implementar os métodos UIGestureRecognizerDelegate que falham nesse gesto e permitir o reconhecimento simultâneo (você pode ou não precisar implementá-lo, isso depende de outros reconhecedores de gesto que você tem ou dependências com reconhecedores de gesto internos)

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

    return NO;
}

credenciais para isso vão para a implementação interna de LXReorderableCollectionViewFlowLayout

Dmitry Fantastik
fonte
Esta é a mesma dança que fiz para meu clone do Snapchat. ahhh amor verdadeiro.
benjaminhallock
1

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)")
    }
}

Além disso, não se esqueça de implementar UIGestureRecognizerDelegate e chamar setupLongGestureRecognizerOnCollection de viewDidLoad ou onde quer que você precise chamá-lo.

Renexandro
fonte
0

Talvez, usar UILongPressGestureRecognizer seja a solução mais difundida. Mas eu encontro com ele dois problemas irritantes:

  • às vezes, esse reconhecedor funciona de maneira incorreta quando movemos nosso toque;
  • reconhecedor intercepta outras ações de toque, portanto, não podemos usar callbacks de destaque de nosso UICollectionView de maneira adequada.

Deixe-me sugerir um pouco de força bruta, mas funcionando como é obrigatório, sugestão:

Declarando uma descrição de retorno de chamada para um clique longo em nosso celular:

typealias OnLongClickListener = (view: OurCellView) -> Void

Estendendo UICollectionViewCell com variáveis ​​(podemos chamá-lo de OurCellView, por exemplo):

/// 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

Adicionando dois métodos em nossa classe de célula:

/**
 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)
}

E substituindo eventos de toque aqui:

/// 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)
}

Então, em algum lugar no controlador de nossa visualização de coleção declarando listener de retorno de chamada:

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

E, finalmente, em cellForItemAtIndexPath definindo callback para nossas células:

/// 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
}

Agora podemos interceptar longas ações de clique em nossas células.

Andrei K.
fonte