web-dev-qa-db-ja.com

コレクションビューのセルのUIButtonがイベント内でタッチアップを受け取らない

次のコードは私の問題を表しています:(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を作成しています。私のICollectionViewCellサブクラス(Cell)には、セルのcontentView(つまり、そのスーパービューの境界。

問題は、contentView境界(基本的にはボタンの3/4)の外側にあるボタンの任意の部分をクリックしてもボタンアクションが呼び出されないことです。 contentViewと重なるボタンの部分をクリックしたときにのみ、ボタンのアクションメソッドが呼び出されます。

Cell-pointInside:withEvent:メソッドをオーバーライドして、ボタンへのタッチが認識されるようにしました。しかし、それはボタンのクリックの問題を解決しませんでした。

collectionViewがタッチをどのように処理するかと関係があるのではないかと思いますが、私にはわかりません。私はICollectionViewIScrollViewサブクラスであることを知っており、部分的に重複するビューを含むビュー(スクロールビューにサブビューを作成)での-pointInside:withEvent:のオーバーライドを実際にテストしましたボタンはボタンのクリック問題を解決しますが、ここでは機能しません。

何か助けは?

**追加:記録のために、問題に対する私の現在の解決策は、セルに外観を与えるcontentViewに小さなサブビューを挿入することです。削除ボタンはcontentViewに追加され、その四角形は実際にはcontentViewの境界内にありますが、セルの表示部分(つまり、差し込みサブビュー)。だから私は私が望んだ効果を得て、ボタンは適切に機能しています。しかし、私はまだ上記の元の実装の問題に興味があります。

23
Aky

問題は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は、ポイントがレシーバーの座標空間内にあると想定しているため、ボタンでこれらのメソッドを呼び出す前にポイントを変換する必要があることに注意してください。

25
honus

インターフェイスビルダーで、オブジェクトをUICollectionViewCellとして設定しましたか?誤って1回UIViewを設定し、それに適切なUICollectionViewCellクラスを割り当てた後ですが、これを行う(ボタン、ラベルなど)はcontentViewに追加されないため、期待どおりに応答しません...

したがって、インターフェイスを描画するときは、UICollectionViewCellオブジェクトを取得するようにIBで思い出してください:)

13
jerrygdm

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)

}

それだけです...すべてのサブビュー

5
Kvant

サブクラス化された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;
}

ストーリーボードに追加されたボタンが機能しないことを認識した後、コードにボタンを追加しました。これが最新のXcodeで修正されているかどうかはわかりません。

お役に立てば幸いです。

3
Daniel Nordh

受け入れられた回答が要求されたので、セル内のタッチを受け取るために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)
}
2
cem olcay

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]; 
}
1
Boaz Saragossi

元の回答の2つのSwiftコンバージョンが正確に一致していないSwiftコンバージョンです。したがって、Swift 4元の回答を変換したいので、誰もがそれを使用できます。 subclassedUICollectionViewCellにコードを貼り付けるだけです。自分のボタンで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
NoSixties

私の意見では、ここでHonusが最良の答えを持っています。実際、私のために機能したのは1つだけなので、他の同様の質問に答えて、次のように送信しました。

UICollectionViewの内部が機能していないUIButtonの解決策を探すために何時間もWebを調査しました。うまくいく解決策がようやく見つかるまで、私を狂わせる。そして、それは適切な方法でもあると思います。ヒットテストをハッキングすることです。これは、UICollectionView Buttonの問題を修正するよりも深く(意図的に)解決できるソリューションです。他のビューの下に埋め込まれたボタンにクリックイベントを送り、イベントの通過をブロックするのに役立ちます。

コレクションビューのセルのUIButtonがイベント内で修正を受け取らない

SO答えはObjective Cにあったので、そこからの手がかりをたどってSwiftソリューションを見つけました:

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

-

セルでのユーザーインタラクション、または私が試した他のさまざまな回答を無効にしても、何も機能しませんでした。

上記で投稿したソリューションの優れた点は、addTargetのセレクター関数とセレクター関数を、慣れている方法のままにしておくことができることです。 touchイベントが目的地に到達するのを助けるために、1つの関数をオーバーライドするだけで済みます。

ソリューションが機能する理由:

最初の数時間は、ジェスチャーがaddTarget呼び出しで適切に登録されていないことがわかりました。ターゲットはうまく登録していることがわかりました。タッチイベントがボタンに届かなかっただけです。

現実は、SO私が読んだ記事や記事の数に由来しているようです。UICollectionViewCellsは、さまざまな理由で複数ではなく、1つのアクションを格納するためのものでした。したがって、supposedは組み込みの選択アクションを使用することを想定しています。そのことを念頭に置いて、properこの制限を回避する方法は、UICollectionViewをハックして、スクロールやユーザー操作の特定の側面を無効にすることではありません。UICollectionViewは、その役割を果たしているだけです。properの方法は、 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)
0
xxxx