集合视图中的单元格中的UIButton未接收到内部事件的修饰

时间:2022-05-02 08:30:48

The following code expresses my problem: (It's self-contained in that you could create a Xcode project with an empty template, replace the contents of the main.m file, delete the AppDelegate.h/.m files and build it)

下面的代码表达了我的问题:(它是自包含的,您可以创建一个带有空模板的Xcode项目,替换main.m文件的内容,删除AppDelegate.h / .m文件并构建它)

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

Basically I'm creating a Springboard-type UI using collection views. My UICollectionViewCell subclass (Cell) has a button which lies partially outside the cell's contentView (i.e. its superview's) bounds.

基本上我正在使用集合视图创建一个Springboard类型的UI。我的UICollectionViewCell子类(Cell)有一个按钮,它位于单元格的contentView(即其superview的)边界之外。

The problem is that clicking on any part of the button outside of the contentView bounds (basically 3/4th of the button) doesn't invoke the button action. Only when clicking on the portion of the button that overlaps the contentView is the button's action method called.

问题是单击contentView边界之外的按钮的任何部分(基本上是按钮的3/4)不会调用按钮操作。仅当单击与contentView重叠的按钮部分时,才会调用按钮的操作方法。

I've even overridden -pointInside:withEvent: method in Cell so that touches in the button will be acknowledged. But that hasn't helped with the button clicking problem.

我甚至在Cell中覆盖-pointInside:withEvent:方法,以便确认按钮中的触摸。但这对按钮点击问题没有帮助。

I'm guessing it might be something to do with how collectionView handles touches, but I don't know what. I know that UICollectionView is a UIScrollView subclass and I've actually tested that overriding -pointInside:withEvent: on a view (made subview to a scroll view) containing a partially overlapping button solves the button clicking problem, but it hasn't worked here.

我猜它可能与collectionView如何处理触摸有关,但我不知道是什么。我知道UICollectionView是一个UIScrollView子类,我实际上已经测试了覆盖-pointInside:withEvent:在包含部分重叠按钮的视图(滚动视图的子视图)上解决了按钮点击问题,但它在这里没有工作。

Any help?

有帮助吗?

** Added: For the record, my current solution to the problem involves insetting a smaller subview to contentView which gives the cell its appearance. The delete button is added to the contentView such that its rect actually lies within the bounds of contentView but only partially overlaps the visible part of the cell (i.e. the inset subview). So I've got the effect I wanted, and the button is working properly. But I'm still curious about the problem with the original implementation above.

**补充:为了记录,我目前解决问题的方法是将一个较小的子视图嵌入到contentView中,从而为该单元提供外观。删除按钮被添加到contentView,使得其rect实际上位于contentView的边界内,但仅部分地重叠单元的可见部分(即插入子视图)。所以我得到了我想要的效果,按钮工作正常。但我仍然对上面原始实现的问题感到好奇。

7 个解决方案

#1


23  

The problem appears to be with hitTest/pointInside. I'm guessing the cell is returning NO from pointInside if the touch is on the part of the button that is outside the cell and thus the button doesn't get hit tested. To fix this you have to override pointInside on your UICollectionViewCell subclass to take the button into account. You also need to override hitTest to return the button if the touch is inside the button. Here are example implementations assuming your button is in a property in the UICollectionViewCell subclass called deleteButton.

问题似乎与hitTest / pointInside有关。我猜测单元格是从pointInside返回NO,如果触摸位于单元格外部的按钮部分,因此按钮不会受到测试。要解决此问题,您必须覆盖UICollectionViewCell子类上的pointInside以将该按钮考虑在内。如果触摸在按钮内,您还需要覆盖hitTest以返回按钮。下面是示例实现,假设您的按钮位于名为deleteButton的UICollectionViewCell子类中的属性中。

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

Note that because hitTest and pointInside expect the point to be in the coordinate space of the receiver you have to remember to convert the point before calling those methods on the button.

请注意,因为hitTest和pointInside期望该点位于接收器的坐标空间中,所以必须记住在按钮上调用这些方法之前转换该点。

#2


11  

In Interface Builder do you have set the object as UICollectionViewCell? Because erroneously one time I set a UIView and after assign to it the correct UICollectionViewCell class...but doing this things (buttons, labels, ecc.) are not added tor the contentView so they don't respond as they would...

在Interface Builder中,您是否将对象设置为UICollectionViewCell?因为错误的一次我设置了一个UIView并在为它分配了正确的UICollectionViewCell类之后......但是这些东西(按钮,标签,ecc。)没有被添加到contentView中,因此它们不会像它们那样响应......

So, remind in IB to take the UICollectionViewCell Object when drawing the interface :)

因此,在IB中提醒在绘制界面时采用UICollectionViewCell对象:)

#3


5  

Swift version:

Swift版本:

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)

}

that's it ... for all subViews

这就是...对于所有子视图

#4


4  

I am successfully receiving touches to a button created as follows in the subclassed UICollectionViewCell.m file;

我成功接收到子类UICollectionViewCell.m文件中按如下方式创建的按钮的触摸;

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

I added the button in code after realising that buttons added in Storyboard did not work, not sure if this is fixed in latest Xcode.

我意识到在Storyboard中添加的按钮不起作用,我在代码中添加了按钮,不确定是否在最新的Xcode中修复了这个。

Hope that helps.

希望有所帮助。

#5


1  

I had a similar problem trying to place a deletion button outside the bounds of a uicollectionview cell and it didn't seam to respond to tap events.

我有一个类似的问题,试图在uicollectionview单元格的边界外放置一个删除按钮,它没有接缝来响应点击事件。

the way i solved it was to place a UITapGestureRecognizer on the collection and when a tap happend preform the following code

我解决它的方式是在集合上放置一个UITapGestureRecognizer,当点击发生时,预先形成以下代码

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

#6


1  

As accepted answer requested, we should make a hitTest in order to recieve touches inside the cell. Here is the Swift 4 code for hit test:

根据要求接受的答案,我们应该制作一个hitTest以接收细胞内的触摸。这是命中测试的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)
}

#7


0  

I see two swift conversions of the original answer that aren't exactly swift conversions. So I just want to give the Swift 4 conversion of the original answer so everyone who wants to can use it. You can just paste the code into your subclassed UICollectionViewCell. Just make sure that you change closeButton with your own button.

我看到两个原始答案的快速转换并不是完全快速的转换。所以我只想给原始答案的Swift 4转换,以便每个想要使用它的人。您只需将代码粘贴到子类UICollectionViewCell即可。只需确保使用您自己的按钮更改closeButton。

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

#1


23  

The problem appears to be with hitTest/pointInside. I'm guessing the cell is returning NO from pointInside if the touch is on the part of the button that is outside the cell and thus the button doesn't get hit tested. To fix this you have to override pointInside on your UICollectionViewCell subclass to take the button into account. You also need to override hitTest to return the button if the touch is inside the button. Here are example implementations assuming your button is in a property in the UICollectionViewCell subclass called deleteButton.

问题似乎与hitTest / pointInside有关。我猜测单元格是从pointInside返回NO,如果触摸位于单元格外部的按钮部分,因此按钮不会受到测试。要解决此问题,您必须覆盖UICollectionViewCell子类上的pointInside以将该按钮考虑在内。如果触摸在按钮内,您还需要覆盖hitTest以返回按钮。下面是示例实现,假设您的按钮位于名为deleteButton的UICollectionViewCell子类中的属性中。

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

Note that because hitTest and pointInside expect the point to be in the coordinate space of the receiver you have to remember to convert the point before calling those methods on the button.

请注意,因为hitTest和pointInside期望该点位于接收器的坐标空间中,所以必须记住在按钮上调用这些方法之前转换该点。

#2


11  

In Interface Builder do you have set the object as UICollectionViewCell? Because erroneously one time I set a UIView and after assign to it the correct UICollectionViewCell class...but doing this things (buttons, labels, ecc.) are not added tor the contentView so they don't respond as they would...

在Interface Builder中,您是否将对象设置为UICollectionViewCell?因为错误的一次我设置了一个UIView并在为它分配了正确的UICollectionViewCell类之后......但是这些东西(按钮,标签,ecc。)没有被添加到contentView中,因此它们不会像它们那样响应......

So, remind in IB to take the UICollectionViewCell Object when drawing the interface :)

因此,在IB中提醒在绘制界面时采用UICollectionViewCell对象:)

#3


5  

Swift version:

Swift版本:

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)

}

that's it ... for all subViews

这就是...对于所有子视图

#4


4  

I am successfully receiving touches to a button created as follows in the subclassed UICollectionViewCell.m file;

我成功接收到子类UICollectionViewCell.m文件中按如下方式创建的按钮的触摸;

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

I added the button in code after realising that buttons added in Storyboard did not work, not sure if this is fixed in latest Xcode.

我意识到在Storyboard中添加的按钮不起作用,我在代码中添加了按钮,不确定是否在最新的Xcode中修复了这个。

Hope that helps.

希望有所帮助。

#5


1  

I had a similar problem trying to place a deletion button outside the bounds of a uicollectionview cell and it didn't seam to respond to tap events.

我有一个类似的问题,试图在uicollectionview单元格的边界外放置一个删除按钮,它没有接缝来响应点击事件。

the way i solved it was to place a UITapGestureRecognizer on the collection and when a tap happend preform the following code

我解决它的方式是在集合上放置一个UITapGestureRecognizer,当点击发生时,预先形成以下代码

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

#6


1  

As accepted answer requested, we should make a hitTest in order to recieve touches inside the cell. Here is the Swift 4 code for hit test:

根据要求接受的答案,我们应该制作一个hitTest以接收细胞内的触摸。这是命中测试的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)
}

#7


0  

I see two swift conversions of the original answer that aren't exactly swift conversions. So I just want to give the Swift 4 conversion of the original answer so everyone who wants to can use it. You can just paste the code into your subclassed UICollectionViewCell. Just make sure that you change closeButton with your own button.

我看到两个原始答案的快速转换并不是完全快速的转换。所以我只想给原始答案的Swift 4转换,以便每个想要使用它的人。您只需将代码粘贴到子类UICollectionViewCell即可。只需确保使用您自己的按钮更改closeButton。

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