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

时间:2012-11-04 11:44:37

标签: ios uicollectionview uiresponder

以下代码表达了我的问题: (它是自包含的,您可以创建一个带有空模板的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]));
    }
}

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

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

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

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

任何帮助?

**新增: 为了记录,我目前解决问题的方法是将较小的子视图插入 contentView ,这样可以使单元格显示出来。删除按钮被添加到 contentView ,使得其rect实际上位于contentView的边界内,但仅部分地重叠单元格的可见部分(即插入子视图)。所以我得到了我想要的效果,按钮工作正常。但是我仍然对上面原始实现的问题感到好奇。

7 个答案:

答案 0 :(得分:24)

问题似乎与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];
}

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

答案 1 :(得分:11)

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

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

答案 2 :(得分:5)

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)

}

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

答案 3 :(得分:3)

我已成功接收到在子类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;
}

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

希望有所帮助。

答案 4 :(得分:1)

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

我解决它的方法是在集合上放置一个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]; 
}

答案 5 :(得分:0)

我看到两个原始答案的快速转换并非完全快速转换。所以我只想对原始答案进行Swift 4转换,以便每个想要使用它的人都可以。您只需将代码粘贴到subclassed 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)
}

答案 6 :(得分:0)

我认为,Honus在这里是最好的答案。实际上,这是唯一对我有用的,所以我一直在回答其他类似的问题,并以这种方式发送它们:

我花了数小时在网上搜寻有关UIButton内部UICollectionView无法正常工作的解决方案。让我发疯,直到我终于找到了适合我的解决方案。而且我相信这也是正确的方法:破解热门测试。与解决UICollectionView Button问题相比,此解决方案可以更深入(双关语意),因为它可以帮助您将click事件传递到掩藏在阻止事件通过的其他视图下的任何按钮:

UIButton in cell in collection view not receiving touch up inside event

由于SO答案在目标C中,所以我从那里的线索中找到了快速解决方案:

http://khanlou.com/2018/09/hacking-hit-tests/

-

当我要禁用单元格上的用户交互或尝试的其他任何答案时,没有任何效果。

我上面发布的解决方案的美丽之处在于,您可以按照自己的习惯保留addTarget和选择器函数的功能,因为它们最有可能永远不会成为问题。您只需要重写一个功能即可帮助触摸事件到达目的地。

解决方案为何起作用:

在开始的几个小时里,我发现我的addTarget调用未正确记录该手势。事实证明,目标注册良好。触摸事件根本无法到达我的按钮。

现实似乎是从我读过的许多SO帖子和文章中得知,UICollectionView单元旨在容纳一个动作,由于各种原因而不是多个。因此,仅应该使用内置的选择操作。考虑到这一点,我相信绕过此限制的正确方法不是破解UICollectionView来禁用滚动或用户交互的某些方面。 UICollectionView仅在执行其工作。 正确的方法是破解点击测试,以在点击进入UICollectionView之前拦截该点击,并弄清他们正在点击哪些项目。然后,您只需将触摸事件发送到他们所点击的按钮,然后让您的普通东西完成工作即可。


我的最终解决方案(来自khanlou.com文章)是将我的addTarget声明和选择器函数放在我喜欢的任何位置(在单元类或cellForItemAt中),并在单元类中覆盖hitTest函数。

在我的细胞班里,我有:

@objc func didTapMyButton(sender:UIButton!) {
    print("Tapped it!")
}

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

    guard isUserInteractionEnabled else { return nil }

    guard !isHidden else { return nil }

    guard alpha >= 0.01 else { return nil }

    guard self.point(inside: point, with: event) else { return nil }


    // add one of these blocks for each button in our collection view cell we want to actually work
    if self.myButton.point(inside: convert(point, to: myButton), with: event) {
        return self.myButton
    }

    return super.hitTest(point, with: event)
}

在单元格初始化中,我有:

self.myButton.addTarget(self, action: #selector(didTapMyButton), for: .touchUpInside)