Подтвердить что ты не робот

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 rect is partially outside the parent-view 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 с использованием представлений коллекции. Мой подкласс UICollectionViewCell (Ячейка) имеет кнопку, которая частично находится за пределами содержимого содержимого cellView (т.е. Его супервизора).

Проблема заключается в том, что нажатие любой части кнопки за пределами границ contentView (в основном 3/4 кнопки) не вызывает действие кнопки. Только при нажатии на часть кнопки, которая перекрывает ContentView, вызывается метод действия кнопки.

Я даже переопределил метод -pointInside:withEvent: в Ячейке, так что прикосновения к кнопке будут подтверждены. Но это не помогло с проблемой нажатия кнопки.

Я предполагаю, что это может быть связано с тем, как collectionView обрабатывает касания, но я не знаю, что. Я знаю, что UICollectionView - это подкласс UIScrollView, и я действительно протестировал это переопределение -pointInside:withEvent: в представлении (сделанное subview в виде прокрутки), содержащее частично перекрывающуюся кнопку, проблема с нажатием кнопки, но она не работает здесь.

Любая помощь?

** Добавлено: Для записи мое текущее решение проблемы включает в себя вложение меньшего subview в contentView, который дает ячейке ее внешний вид. Кнопка delete добавляется в contentView таким образом, что ее прямой фактически лежит в пределах contentView, но только частично перекрывает видимую часть ячейки (т.е. Вложенное представление). Итак, у меня есть эффект, который я хотел, и кнопка работает правильно. Но мне все еще интересно узнать о проблеме с первоначальной реализацией выше.

4b9b3361

Ответ 1

Проблема связана с hitTest/pointInside. Я предполагаю, что ячейка возвращает NO из pointInside, если касание находится на стороне кнопки, находящейся вне ячейки, и, таким образом, кнопка не попадает под удар. Чтобы исправить это, вы должны переопределить pointInside в своем подклассе UICollectionViewCell, чтобы включить эту кнопку. Вам также необходимо переопределить 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];
}

Обратите внимание, что поскольку hitTest и pointInside ожидают, что точка находится в координатном пространстве приемника, вы должны помнить, чтобы преобразовать точку до вызова этих методов на кнопке.

Ответ 2

В интерфейсе Builder вы установили объект как UICollectionViewCell? Поскольку ошибочно один раз я установил UIView и после этого назначил ему правильный класс UICollectionViewCell... но делать это (кнопки, метки, ecc.) Не добавляются в contentView, поэтому они не отвечают так, как они бы...

Итак, напомните в IB, чтобы взять объект UICollectionViewCell при рисовании интерфейса:)

Ответ 3

Быстрая версия:

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)

}

что он... для всех подвидных объектов

Ответ 4

Я успешно получаю штрихи к кнопке, созданной следующим образом в файле подкласса 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.

Надеюсь, что это поможет.

Ответ 5

У меня была аналогичная проблема, пытаясь поместить кнопку удаления за пределы ячейки 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]; 
}

Ответ 6

В соответствии с принятым ответом на запрос, мы должны выполнить hitTest, чтобы 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

Я вижу два быстрых преобразования первоначального ответа, которые не совсем быстрые преобразования. Так что я просто хочу дать 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)
}

Ответ 8

У Хонуса есть лучший ответ здесь, на мой взгляд. На самом деле единственный, который работал на меня, поэтому я отвечал на другие подобные вопросы и отправлял их следующим образом:

Я часами рыскал по сети, чтобы найти решение для моего UIButton внутри UICollectionView, который не работает. Меня сводило с ума, пока я, наконец, не нашел решение, которое работает для меня. И я считаю, что это также правильный путь: взломать тесты на попадание. Это решение, которое может пойти гораздо глубже (как каламбур), чем исправить проблемы с кнопкой UICollectionView, так как оно может помочь вам передать событие click любой кнопке, скрытой под другими представлениями, которые блокируют ваши события от прохождения:

Кнопка UIB в ячейке в представлении коллекции не получает завершение внутри события

Так как этот SO-ответ был в Задаче C, я последовал подсказкам оттуда, чтобы найти быстрое решение:

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

-

Когда я отключал взаимодействие с пользователем в ячейке или любые другие варианты ответов, которые я пробовал, ничего не получалось.

Прелесть решения, которое я опубликовал выше, состоит в том, что вы можете оставить свои функции addTarget и selector такими, какими вы привыкли их делать, поскольку они, скорее всего, никогда не были проблемой. Вам нужно переопределить только одну функцию, чтобы помочь сенсорному событию добраться до места назначения.

Почему решение работает:

Первые несколько часов я полагал, что жест не регистрируется должным образом с моими вызовами 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)
}

И в моем классе ячейки init у меня есть:

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