web-dev-qa-db-fra.com

UIButton dans la cellule de la vue de collection ne recevant pas de retouche dans l'événement

Le code suivant exprime mon problème: (Il est autonome, vous pouvez créer un projet Xcode avec un modèle vide, remplacer le contenu du fichier main.m, supprimer les fichiers AppDelegate.h/.m et construit le)

//
//  main.m
//  CollectionViewProblem
//


#import <UIKit/UIKit.h>

@interface Cell : UICollectionViewCell

@property (nonatomic, strong) UIButton *button;
@property (nonatomic, strong) UILabel *label;

@end

@implementation Cell
 - (id)initWithFrame:(CGRect)frame
{
    if (self = [super initWithFrame:frame])
    {
        self.label = [[UILabel alloc] initWithFrame:self.bounds];
        self.label.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
        self.label.backgroundColor = [UIColor greenColor];
        self.label.textAlignment = NSTextAlignmentCenter;

        self.button = [UIButton buttonWithType:UIButtonTypeInfoLight]; 
        self.button.frame = CGRectMake(-frame.size.width/4, -frame.size.width/4, frame.size.width/2, frame.size.width/2);
        self.button.backgroundColor = [UIColor redColor];
        [self.button addTarget:self action:@selector(buttonClicked:) forControlEvents:UIControlEventTouchUpInside];
        [self.contentView addSubview:self.label];
        [self.contentView addSubview:self.button];
    }
    return self;
}


// Overriding this because the button's rect is partially outside the parent-view's bounds:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    if ([super pointInside:point withEvent:event])
    {
        NSLog(@"inside cell");
        return YES;
    }
    if ([self.button
         pointInside:[self convertPoint:point
                                 toView:self.button] withEvent:nil])
    {
        NSLog(@"inside button");
        return YES;
    }

    return NO;
}


- (void)buttonClicked:(UIButton *)sender
{
    NSLog(@"button clicked!");
}
@end

@interface ViewController : UICollectionViewController

@end

@implementation ViewController

// (1a) viewdidLoad:

- (void)viewDidLoad
{
    [super viewDidLoad];

    [self.collectionView registerClass:[Cell class] forCellWithReuseIdentifier:@"ID"];
}

// collection view data source methods ////////////////////////////////////

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    return 100;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    Cell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"ID" forIndexPath:indexPath];
    cell.label.text = [NSString stringWithFormat:@"%d", indexPath.row];
    return cell;
}
///////////////////////////////////////////////////////////////////////////

// collection view delegate methods ////////////////////////////////////////

- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
    NSLog(@"cell #%d was selected", indexPath.row);
}
////////////////////////////////////////////////////////////////////////////
@end


@interface AppDelegate : UIResponder <UIApplicationDelegate>

@property (strong, nonatomic) UIWindow *window;

@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];

    UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
    ViewController *vc = [[ViewController alloc] initWithCollectionViewLayout:layout];


    layout.itemSize = CGSizeMake(128, 128);
    layout.minimumInteritemSpacing = 64;
    layout.minimumLineSpacing = 64;
    layout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
    layout.sectionInset = UIEdgeInsetsMake(32, 32, 32, 32);


    self.window.rootViewController = vc;

    self.window.backgroundColor = [UIColor whiteColor];
    [self.window makeKeyAndVisible];
    return YES;
}

@end


int main(int argc, char *argv[])
{
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

Fondamentalement, je crée une interface utilisateur de type Springboard à l'aide de vues de collection. Ma sous-classe UICollectionViewCell (Cellule) a un bouton qui se situe partiellement en dehors des limites de la cellule contentView (c'est-à-dire de son superview).

Le problème est que cliquer sur une partie du bouton en dehors de contentView (essentiellement les 3/4 du bouton) n'appelle pas l'action du bouton. Lorsque vous cliquez sur la partie du bouton qui chevauche le contentView , la méthode d'action du bouton est appelée.

J'ai même surchargé la méthode -pointInside:withEvent: dans Cell pour que les contacts dans le bouton soient reconnus. Mais cela n'a pas aidé avec le problème de clic de bouton. 

Je suppose que cela pourrait être lié à la façon dont collectionView gère les contacts, mais je ne sais pas quoi. Je sais que UICollectionView _ est une sous-classe UIScrollView, et j'ai en fait testé que le fait de surcharger -pointInside:withEvent: sur une vue (faite d'une sous-vue à défilement) contenant un bouton partiellement imbriqué résout le problème de clic du bouton. , mais cela n'a pas fonctionné ici.

De l'aide?

** Ajouté: Pour mémoire, ma solution actuelle au problème consiste à insérer une sous-vue plus petite dans contentView qui donne à la cellule son apparence. Le bouton de suppression est ajouté à contentView de sorte que son rect se situe réellement dans les limites de contentView mais ne recouvre que partiellement la partie visible de la cellule (c.-à-d. La sous-vue incrustée). Donc, j'ai l'effet que je voulais et le bouton fonctionne correctement. Mais je suis toujours curieux du problème avec la mise en œuvre originale ci-dessus.

21
Aky

Le problème semble être avec hitTest/pointInside. Je suppose que la cellule renvoie NON de PointInside si le contact est exercé sur la partie du bouton qui se trouve en dehors de la cellule et que, par conséquent, le bouton n'est pas testé. Pour résoudre ce problème, vous devez remplacer pointInside sur votre sous-classe UICollectionViewCell pour prendre en compte le bouton. Vous devez également remplacer hitTest pour renvoyer le bouton si le contact est à l'intérieur du bouton. Voici des exemples d'implémentations en supposant que votre bouton se trouve dans une propriété de la sous-classe UICollectionViewCell appelée deleteButton.

-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    UIView *view = [self.deleteButton hitTest:[self.deleteButton convertPoint:point fromView:self] withEvent:event];
    if (view == nil) {
        view = [super hitTest:point withEvent:event];
    }
    return view;
}

-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    if ([super pointInside:point withEvent:event]) {
        return YES;
    }
    //Check to see if it is within the delete button
    return !self.deleteButton.hidden && [self.deleteButton pointInside:[self.deleteButton convertPoint:point fromView:self] withEvent:event];
}

Notez que, parce que hitTest et pointInside s'attendent à ce que le point se trouve dans l'espace de coordonnées du récepteur, vous devez vous rappeler de convertir le point avant d'appeler ces méthodes sur le bouton.

23
honus

Avez-vous défini l'objet avec UICollectionViewCell dans Interface Builder? Parce que, à tort, une fois, j'ai défini un UIView et lui ai ensuite assigné la classe UICollectionViewCell correcte ... mais cette opération n'est pas ajoutée (boutons, libellés, etc.) au contenu afin qu'elle ne réponde pas comme ils le feraient ...

Alors rappelez à IB de prendre l’objet UICollectionViewCell lors du dessin de l’interface :)

11
jerrygdm

Version rapide:

override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {

    //From higher z- order to lower except base view;

    for (var i = subviews.count-2; i >= 0 ; i--){
        let newPoint = subviews[i].convertPoint(point, fromView: self)
        let view = subviews[i].hitTest(newPoint, withEvent: event)
        if view != nil{
            return view
        }
    }

    return super.hitTest(point, withEvent: event)

}

ça y est ... pour tous les subViews 

5
Kvant

Je reçois des contacts avec un bouton créé comme suit dans le fichier UICollectionViewCell.m de la sous-classe;

- (id)initWithCoder:(NSCoder *)aDecoder
    {
    self = [super initWithCoder:aDecoder];
    if (self)
    {

    // Create button

    UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
    button.frame = CGRectMake(0, 0, 100, 100); // position in the parent view and set the size of the button
    [button setTitle:@"Title" forState:UIControlStateNormal];
    [button setImage:[UIImage imageNamed:@"animage.png"] forState:UIControlStateNormal];
    [button addTarget:self action:@selector(button:) forControlEvents:UIControlEventTouchUpInside];

    // add to contentView
    [self.contentView addSubview:button];
    }
    return self;
}

J'ai ajouté le bouton dans le code après avoir réalisé que les boutons ajoutés dans Storyboard ne fonctionnaient pas, mais je ne savais pas si cela était corrigé dans le dernier Xcode.

J'espère que cela pourra aider.

3
Daniel Nordh

J'ai eu un problème similaire en essayant de placer un bouton de suppression en dehors des limites d'une cellule uicollectionview et il ne semblait pas possible de répondre aux événements tapant.

la façon dont je l'ai résolu consistait à placer un UITapGestureRecognizer sur la collection et lorsqu'un clic se produisait préformait le code suivant

//this works also on taps outside the cell bouns, im guessing by getting the closest cell to the point of click.

NSIndexPath* tappedCellPath = [self.collectionView indexPathForItemAtPoint:[tapRecognizer locationInView:self.collectionView]]; 

if(tappedCellPath) {
    UICollectionViewCell *tappedCell = [self.collectionView cellForItemAtIndexPath:tappedCellPath];
    CGPoint tapInCellPoint = [tapRecognizer locationInView:tappedCell];
    //if the tap was outside of the cell bounds then its in negative values and it means the delete button was tapped
    if (tapInCellPoint.x < 0) [self deleteCell:tappedCell]; 
}
1
Boaz Saragossi

Comme réponse acceptée demandée, nous devrions créer un hitTest afin de recevoir des contacts à l'intérieur de la cellule. Voici le code de test rapide de Swift 4:

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
  for i in (0..<subviews.count-1).reversed() {
    let newPoint = subviews[i].convert(point, from: self)
    if let view = subviews[i].hitTest(newPoint, with: event) {
        return view
    }
  }
  return super.hitTest(point, with: event)
}
1
cem olcay

Je vois deux conversions Swift de la réponse d'origine qui ne sont pas exactement des conversions Swift. Donc, je veux juste donner la conversion Swift 4 de la réponse originale pour que tous ceux qui le souhaitent puissent l’utiliser. Vous pouvez simplement coller le code dans votre subclassedUICollectionViewCell. Assurez-vous simplement de changer closeButton avec votre propre bouton.

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    var view = closeButton.hitTest(closeButton.convert(point, from: self), with: event)
    if view == nil {
        view = super.hitTest(point, with: event)
    }

    return view
}

override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
    if super.point(inside: point, with: event) {
        return true
    }

    return !closeButton.isHidden && closeButton.point(inside: closeButton.convert(point, from: self), with: event)
}
0
NoSixties