ios 集合视图中单元格中的 UIButton 未在事件内部接收修饰
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/13218208/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
UIButton in cell in collection view not receiving touch up inside event
提问by Aky
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 UICollectionViewCellsubclass (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 contentViewbounds (basically 3/4th of the button) doesn't invoke the button action. Only when clicking on the portion of the button that overlaps the contentViewis the button's action method called.
问题是单击contentView边界之外的按钮的任何部分(基本上是按钮的 3/4)不会调用按钮操作。只有在单击与contentView重叠的按钮部分时,才会调用按钮的操作方法。
I've even overridden -pointInside:withEvent:
method in Cellso that touches in the button will be acknowledged. But that hasn't helped with the button clicking problem.
我什至-pointInside:withEvent:
在Cell 中重写了方法,以便确认按钮中的触摸。但这对按钮点击问题没有帮助。
I'm guessing it might be something to do with how collectionViewhandles touches, but I don't know what. I know that UICollectionViewis a UIScrollViewsubclass 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 contentViewwhich gives the cell its appearance. The delete button is added to the contentViewsuch 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 的边界内,但仅与单元格的可见部分(即插入子视图)部分重叠。所以我得到了我想要的效果,并且按钮工作正常。但我仍然对上面原始实现的问题感到好奇。
回答by honus
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 以返回按钮。以下示例实现假设您的按钮位于 UICollectionViewCell 子类中名为 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];
}
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 期望点位于接收器的坐标空间中,所以您必须记住在按钮上调用这些方法之前转换点。
回答by jerrygdm
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 对象:)
回答by Kvant
Swift version:
迅捷版:
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
就是这样......对于所有子视图
回答by Daniel Nordh
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.
希望有帮助。
回答by cem olcay
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)
}
回答by NoSixties
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
原始答案的转换,以便每个想要使用它的人都可以使用它。您只需将代码粘贴到您的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)
}
回答by Boaz Saragossi
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];
}
回答by xxxx
Honus has the best answer here in my opinion. In fact the only one that worked for me so I've been answering other similar questions and sending them this way:
在我看来,Honus 在这里给出了最好的答案。事实上,这是唯一对我有用的,所以我一直在回答其他类似的问题并以这种方式发送:
I spent hours scouring the web for a solution to my UIButton's inside UICollectionView's not working. Driving me nuts until I finally found a solution that works for me. And I believe it's also the proper way to go: hacking the hit tests. It's a solution that can go a lot deeper (pun intended) than fixing the UICollectionView Button issues as well, as it can help you get the click event to any button buried under other views that are blocking your events from getting through:
我花了几个小时在网上搜索解决我的 UIButton 在 UICollectionView 中不起作用的方法。让我发疯,直到我终于找到适合我的解决方案。而且我相信这也是正确的方法:破解命中测试。这是一个可以比修复 UICollectionView 按钮问题更深入(双关语)的解决方案,因为它可以帮助您将点击事件发送到隐藏在其他视图下的任何按钮,这些按钮阻止您的事件通过:
UIButton in cell in collection view not receiving touch up inside event
集合视图中单元格中的 UIButton 未在事件内部接收修饰
Since that SO answer was in Objective C, I followed the clues from there to find a swift solution:
由于那个 SO 答案是在目标 C 中,我按照那里的线索找到了一个快速的解决方案:
http://khanlou.com/2018/09/hacking-hit-tests/
http://khanlou.com/2018/09/hacking-hit-tests/
--
——
When I would disable user interaction on the cell, or any other variety of answers I tried, nothing worked.
当我禁用单元格上的用户交互或我尝试过的任何其他各种答案时,没有任何效果。
The beauty of the solution I posted above is that you can leave your addTarget's and selector functions how you are used to doing them since they were most likey never the problem. You need only override one function to help the touch event make it to its destination.
我在上面发布的解决方案的美妙之处在于,您可以保留 addTarget 和选择器函数的习惯,因为它们很可能从来都不是问题。您只需要覆盖一个函数来帮助触摸事件到达目的地。
Why the solution works:
为什么解决方案有效:
For the first few hours I figured the gesture wasn't being registered properly with my addTarget calls. It turns out the targets were registering fine. The touch events were simply never reaching my buttons.
在最初的几个小时里,我认为我的 addTarget 调用没有正确注册手势。事实证明,目标注册得很好。触摸事件根本没有到达我的按钮。
The reality seems to be from any number of SO posts and articles I read, that UICollectionView Cells were meant to house one action, not multiple for a variety of reasons. So you were only supposedto be using the built in selection actions. With that in mind, I believe the properway around this limitation is not to hack UICollectionView to disable certain aspects of scrolling or user interaction. UICollectionView is only doing its job. The properway is to hack the hit tests to intercept the tap before it gets to UICollectionView and figure out which items they were tapping on. Then you simply send a touch event to the button they were tapping on, and let your normal stuff do the work.
现实似乎来自我读过的任何数量的 SO 帖子和文章,UICollectionView Cells 旨在容纳一个动作,而不是多个动作。所以你应该只使用内置的选择操作。考虑到这一点,我认为解决此限制的正确方法不是破解 UICollectionView 以禁用滚动或用户交互的某些方面。UICollectionView 只是在做它的工作。在正确的方式是破解命中测试拦截它得到UICollectionView之前哪些项目,他们攻水龙头和人物。然后您只需向他们正在点击的按钮发送一个触摸事件,让您的普通东西完成工作。
My final solution (from the khanlou.com article) is to put my addTarget declaration and my selector function wherever I like (in the cell class or the cellForItemAt override), and in the cell class overriding the hitTest function.
我的最终解决方案(来自 khanlou.com 文章)是将我的 addTarget 声明和我的选择器函数放在我喜欢的任何地方(在单元格类或 cellForItemAt 覆盖中),并在覆盖 hitTest 函数的单元格类中。
In my cell class I have:
在我的细胞课中,我有:
@objc func didTapMyButton(sender:UIButton!) {
print("Tapped it!")
}
and
和
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)
}
And in my cell class init I have:
在我的单元类 init 中,我有:
self.myButton.addTarget(self, action: #selector(didTapMyButton), for: .touchUpInside)