Как реализовать прокрутку UITableView для удаления для UICollectionView

Мне просто интересно спросить, как я могу реализовать одно и то же поведение UITableView `s swipe для удаления в UICollectionView. Я пытаюсь найти учебник, но я не могу найти его.

Кроме того, я использую оболочку PSTCollectionView для поддержки iOS 5.

Спасибо!

Изменить: Улавливатель салфеток уже хорош. Теперь мне нужны те же функции, что и UITableView при отмене режима удаления, например. когда пользователь нажимает на ячейку или на пустое место в представлении таблицы (то есть, когда пользователь выходит за пределы кнопки "Удалить" ). UITapGestureRecognizer не будет работать, поскольку он только обнаруживает нажатия на разблокирование касания. UITableView обнаруживает прикосновение к началу жестов (а не к выпуску) и немедленно отменяет режим удаления.

Ответ 1

В Руководстве по программированию коллекции для iOS в разделе Включение поддержки жестов, документы читают:

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

Итак, я считаю, что не очень хорошая практика добавлять распознавателей к UICollectionViewCell.

Ответ 2

Это очень просто. Вам нужно добавить customContentView и customBackgroundView за свой customContentView.

После этого вам нужно переместить customContentView влево, поскольку пользователь выполняет customContentView справа налево. Смещение вида делает видимым для customBackgroundView.

Разрешает код:

Прежде всего вам нужно добавить panGesture в свой UICollectionView как

   override func viewDidLoad() {
        super.viewDidLoad()
        self.panGesture = UIPanGestureRecognizer(target: self, action: #selector(self.panThisCell))
        panGesture.delegate = self
        self.collectionView.addGestureRecognizer(panGesture)

    }

Теперь установите селектор как

  func panThisCell(_ recognizer:UIPanGestureRecognizer){

        if recognizer != panGesture{  return }

        let point = recognizer.location(in: self.collectionView)
        let indexpath = self.collectionView.indexPathForItem(at: point)
        if indexpath == nil{  return }
        guard let cell = self.collectionView.cellForItem(at: indexpath!) as? CustomCollectionViewCell else{

            return

        }
        switch recognizer.state {
        case .began:

            cell.startPoint =  self.collectionView.convert(point, to: cell)
            cell.startingRightLayoutConstraintConstant  = cell.contentViewRightConstraint.constant
            if swipeActiveCell != cell && swipeActiveCell != nil{

                self.resetConstraintToZero(swipeActiveCell!,animate: true, notifyDelegateDidClose: false)
            }
            swipeActiveCell = cell

        case .changed:

            let currentPoint =  self.collectionView.convert(point, to: cell)
            let deltaX = currentPoint.x - cell.startPoint.x
            var panningleft = false

            if currentPoint.x < cell.startPoint.x{

                panningleft = true

            }
            if cell.startingRightLayoutConstraintConstant == 0{

                if !panningleft{

                    let constant = max(-deltaX,0)
                    if constant == 0{

                        self.resetConstraintToZero(cell,animate: true, notifyDelegateDidClose: false)

                    }else{

                        cell.contentViewRightConstraint.constant = constant

                    }
                }else{

                    let constant = min(-deltaX,self.getButtonTotalWidth(cell))
                    if constant == self.getButtonTotalWidth(cell){

                        self.setConstraintsToShowAllButtons(cell,animate: true, notifyDelegateDidOpen: false)

                    }else{

                        cell.contentViewRightConstraint.constant = constant
                        cell.contentViewLeftConstraint.constant = -constant
                    }
                }
            }else{

                let adjustment = cell.startingRightLayoutConstraintConstant - deltaX;
                if (!panningleft) {

                    let constant = max(adjustment, 0);
                    if (constant == 0) {

                        self.resetConstraintToZero(cell,animate: true, notifyDelegateDidClose: false)

                    } else {

                        cell.contentViewRightConstraint.constant = constant;
                    }
                } else {
                    let constant = min(adjustment, self.getButtonTotalWidth(cell));
                    if (constant == self.getButtonTotalWidth(cell)) {

                        self.setConstraintsToShowAllButtons(cell,animate: true, notifyDelegateDidOpen: false)
                    } else {

                        cell.contentViewRightConstraint.constant = constant;
                    }
                }
                cell.contentViewLeftConstraint.constant = -cell.contentViewRightConstraint.constant;

            }
            cell.layoutIfNeeded()
        case .cancelled:

            if (cell.startingRightLayoutConstraintConstant == 0) {

                self.resetConstraintToZero(cell,animate: true, notifyDelegateDidClose: true)

            } else {

                self.setConstraintsToShowAllButtons(cell,animate: true, notifyDelegateDidOpen: true)
            }

        case .ended:

            if (cell.startingRightLayoutConstraintConstant == 0) {
                //Cell was opening
                let halfOfButtonOne = (cell.swipeView.frame).width / 2;
                if (cell.contentViewRightConstraint.constant >= halfOfButtonOne) {
                    //Open all the way
                    self.setConstraintsToShowAllButtons(cell,animate: true, notifyDelegateDidOpen: true)
                } else {
                    //Re-close
                    self.resetConstraintToZero(cell,animate: true, notifyDelegateDidClose: true)
                }
            } else {
                //Cell was closing
                let buttonOnePlusHalfOfButton2 = (cell.swipeView.frame).width
                if (cell.contentViewRightConstraint.constant >= buttonOnePlusHalfOfButton2) {
                    //Re-open all the way
                    self.setConstraintsToShowAllButtons(cell,animate: true, notifyDelegateDidOpen: true)
                } else {
                    //Close
                    self.resetConstraintToZero(cell,animate: true, notifyDelegateDidClose: true)
                }
            }

        default:
            print("default")
        }
    }

Вспомогательные методы для обновления ограничений

 func getButtonTotalWidth(_ cell:CustomCollectionViewCell)->CGFloat{

        let width = cell.frame.width - cell.swipeView.frame.minX
        return width

    }

    func resetConstraintToZero(_ cell:CustomCollectionViewCell, animate:Bool,notifyDelegateDidClose:Bool){

        if (cell.startingRightLayoutConstraintConstant == 0 &&
            cell.contentViewRightConstraint.constant == 0) {
            //Already all the way closed, no bounce necessary
            return;
        }
        cell.contentViewRightConstraint.constant = -kBounceValue;
        cell.contentViewLeftConstraint.constant = kBounceValue;
        self.updateConstraintsIfNeeded(cell,animated: animate) {
            cell.contentViewRightConstraint.constant = 0;
            cell.contentViewLeftConstraint.constant = 0;

            self.updateConstraintsIfNeeded(cell,animated: animate, completionHandler: {

                cell.startingRightLayoutConstraintConstant = cell.contentViewRightConstraint.constant;
            })
        }
        cell.startPoint = CGPoint()
        swipeActiveCell = nil
    }

    func setConstraintsToShowAllButtons(_ cell:CustomCollectionViewCell, animate:Bool,notifyDelegateDidOpen:Bool){

        if (cell.startingRightLayoutConstraintConstant == self.getButtonTotalWidth(cell) &&
            cell.contentViewRightConstraint.constant == self.getButtonTotalWidth(cell)) {
            return;
        }
        cell.contentViewLeftConstraint.constant = -self.getButtonTotalWidth(cell) - kBounceValue;
        cell.contentViewRightConstraint.constant = self.getButtonTotalWidth(cell) + kBounceValue;

        self.updateConstraintsIfNeeded(cell,animated: animate) {
            cell.contentViewLeftConstraint.constant =  -(self.getButtonTotalWidth(cell))
            cell.contentViewRightConstraint.constant = self.getButtonTotalWidth(cell)

            self.updateConstraintsIfNeeded(cell,animated: animate, completionHandler: {(check) in

                cell.startingRightLayoutConstraintConstant = cell.contentViewRightConstraint.constant;
            })
        }
    }

    func setConstraintsAsSwipe(_ cell:CustomCollectionViewCell, animate:Bool,notifyDelegateDidOpen:Bool){

        if (cell.startingRightLayoutConstraintConstant == self.getButtonTotalWidth(cell) &&
            cell.contentViewRightConstraint.constant == self.getButtonTotalWidth(cell)) {
            return;
        }
        cell.contentViewLeftConstraint.constant = -self.getButtonTotalWidth(cell) - kBounceValue;
        cell.contentViewRightConstraint.constant = self.getButtonTotalWidth(cell) + kBounceValue;

        self.updateConstraintsIfNeeded(cell,animated: animate) {
            cell.contentViewLeftConstraint.constant =  -(self.getButtonTotalWidth(cell))
            cell.contentViewRightConstraint.constant = self.getButtonTotalWidth(cell)

            self.updateConstraintsIfNeeded(cell,animated: animate, completionHandler: {(check) in

                cell.startingRightLayoutConstraintConstant = cell.contentViewRightConstraint.constant;
            })
        }
    }


    func updateConstraintsIfNeeded(_ cell:CustomCollectionViewCell, animated:Bool,completionHandler:@escaping ()->()) {
        var duration:Double = 0
        if animated{

            duration = 0.1

        }
        UIView.animate(withDuration: duration, delay: 0, options: [.curveEaseOut], animations: {

            cell.layoutIfNeeded()

            }, completion:{ value in

                if value{ completionHandler() }
        })
    }

Я создал образец проекта здесь, в Swift 3.

Это модифицированная версия этого руководства.

Ответ 3

Я последовал аналогичному подходу к @JacekLampart, но решил добавить UISwipeGestureRecognizer в функцию awakeFromNib UICollectionViewCell, поэтому он добавляется только один раз.

UICollectionViewCell.m

- (void)awakeFromNib {
    UISwipeGestureRecognizer* swipeGestureRecognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipeToDeleteGesture:)];
    swipeGestureRecognizer.direction = UISwipeGestureRecognizerDirectionLeft;
    [self addGestureRecognizer:swipeGestureRecognizer];
}

- (void)swipeToDeleteGesture:(UISwipeGestureRecognizer *)swipeGestureRecognizer {
    if (swipeGestureRecognizer.state == UIGestureRecognizerStateEnded) {
        // update cell to display delete functionality
    }
}

Что касается выхода из режима удаления, я создал пользовательский UIGestureRecognizer с NSArray UIViews. Я заимствовал идею от @iMS из этого вопроса: UITapGestureRecognizer - заставить его работать при касании, а не касаться?

В касанияхBegan, если точка касания не находится ни в одном из UIView, жест становится успешным, и режим удаления завершен.

Таким образом, я могу передать кнопку удаления внутри ячейки (и любых других представлений) в UIGestureRecognizer и, если точка касания находится в рамке кнопки, режим удаления не будет завершен.

TouchDownExcludingViewsGestureRecognizer.h

#import <UIKit/UIKit.h>

@interface TouchDownExcludingViewsGestureRecognizer : UIGestureRecognizer

@property (nonatomic) NSArray *excludeViews;

@end

TouchDownExcludingViewsGestureRecognizer.m

#import "TouchDownExcludingViewsGestureRecognizer.h"
#import <UIKit/UIGestureRecognizerSubclass.h>

@implementation TouchDownExcludingViewsGestureRecognizer

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    if (self.state == UIGestureRecognizerStatePossible) {
        BOOL touchHandled = NO;
        for (UIView *view in self.excludeViews) {
            CGPoint touchLocation = [[touches anyObject] locationInView:view];
            if (CGRectContainsPoint(view.bounds, touchLocation)) {
                touchHandled = YES;
                break;
            }
        }

        self.state = (touchHandled ? UIGestureRecognizerStateFailed : UIGestureRecognizerStateRecognized);
    }
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    self.state = UIGestureRecognizerStateFailed;
}

-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    self.state = UIGestureRecognizerStateFailed;
}


@end

Реализация (в UIViewController, содержащем UICollectionView):

#import "TouchDownExcludingViewsGestureRecognizer.h"

TouchDownExcludingViewsGestureRecognizer *touchDownGestureRecognizer = [[TouchDownExcludingViewsGestureRecognizer alloc] initWithTarget:self action:@selector(exitDeleteMode:)];
touchDownGestureRecognizer.excludeViews = @[self.cellInDeleteMode.deleteButton];
[self.view addGestureRecognizer:touchDownGestureRecognizer];

- (void)exitDeleteMode:(TouchDownExcludingViewsGestureRecognizer *)touchDownGestureRecognizer {
    // exit delete mode and disable or remove TouchDownExcludingViewsGestureRecognizer
}

Ответ 4

Вы можете попробовать добавить UISwipeGestureRecognizer в каждую ячейку коллекции, например:

-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
             cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    CollectionViewCell *cell = ...

    UISwipeGestureRecognizer* gestureRecognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(userDidSwipe:)];
    [gestureRecognizer setDirection:UISwipeGestureRecognizerDirectionRight];
    [cell addGestureRecognizer:gestureRecognizer];
}

а затем:

- (void)userDidSwipe:(UIGestureRecognizer *)gestureRecognizer {
    if (gestureRecognizer.state == UIGestureRecognizerStateEnded) {
        //handle the gesture appropriately
    }
}

Ответ 5

Существует более стандартное решение для реализации этой функции, с поведением, очень похожим на поведение, предоставляемое UITableView.

Для этого вы будете использовать UIScrollView в качестве корневого представления ячейки, а затем поместите содержимое ячейки и кнопку удаления внутри прокрутки. Код в вашем классе ячейки должен выглядеть примерно так:

override init(frame: CGRect) {
    super.init(frame: frame)

    addSubview(scrollView)
    scrollView.addSubview(viewWithCellContent)
    scrollView.addSubview(deleteButton)
    scrollView.isPagingEnabled = true
    scrollView.showsHorizontalScrollIndicator = false
}

В этом коде мы устанавливаем свойство isPagingEnabled true чтобы сделать вид прокрутки, чтобы остановить прокрутку только на границах его содержимого. Подвижности макета для этой ячейки должны быть примерно такими:

override func layoutSubviews() {
    super.layoutSubviews()

    scrollView.frame = bounds
    // make the view with the content to fill the scroll view
    viewWithCellContent.frame = scrollView.bounds
    // position the delete button just at the right of the view with the content.
    deleteButton.frame = CGRect(
        x: label.frame.maxX, 
        y: 0, 
        width: 100, 
        height: scrollView.bounds.height
    )

    // update the size of the scrolleable content of the scroll view
    scrollView.contentSize = CGSize(width: button.frame.maxX, height: scrollView.bounds.height)
}

С помощью этого кода на месте, если вы запустите приложение, вы увидите, что удаление удаленных файлов работает так, как ожидалось, однако мы потеряли возможность выбора ячейки. Проблема заключается в том, что, поскольку представление прокрутки заполняет всю ячейку, все события касания обрабатываются им, поэтому в представлении коллекции никогда не будет возможности выбрать ячейку (это похоже на то, когда у нас есть кнопка внутри ячейки, так как прикосновения к этой кнопке не запускают процесс выбора, а обрабатываются непосредственно кнопкой.)

Чтобы исправить эту проблему, нам просто нужно указать вид прокрутки, чтобы игнорировать события касания, которые обрабатываются им, а не одно из его подзонов. Для этого просто создайте подкласс UIScrollView и переопределите следующую функцию:

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    let result = super.hitTest(point, with: event)
    return result != self ? result : nil
}

Теперь в вашей ячейке вы должны использовать экземпляр этого нового подкласса вместо стандартного UIScrollView.

Если вы запустите приложение сейчас, вы увидите, что у нас есть выбор ячейки назад, но на этот раз салфетка не работает. Поскольку мы игнорируем штрихи, которые обрабатываются непосредственно в режиме прокрутки, тогда распознаватель распознавания жетона не сможет начать распознавать события касания. Тем не менее, это можно легко устранить, указав на вид прокрутки, что его распознаватель жестов будет обрабатываться сотой, а не свитком. Вы делаете это, добавляя следующую строку внизу init(frame: CGRect) ячейки init(frame: CGRect):

addGestureRecognizer(scrollView.panGestureRecognizer)

Это может показаться немного взломанным, но это не так. По дизайну представление, содержащее распознаватель жестов и цель этого распознавателя, не обязательно должно быть одним и тем же объектом.

После этого изменения все должно работать как ожидалось. Вы можете увидеть полную реализацию этой идеи в этом репо

Ответ 6

Существует более простое решение вашей проблемы, которое позволяет избежать использования распознавателей жестов. Решение основано на UIScrollView в сочетании с UIStackView.

  1. Во-первых, вам нужно создать 2 вида контейнера - один для видимой части ячейки и один для скрытой части. Вы добавите эти представления в UIStackView. stackView будет действовать как просмотр содержимого. Убедитесь, что представления имеют равную ширину с помощью stackView.distribution =.fillEqually.

  2. Youll внедряет stackView внутри UIScrollView с включенным пейджингом. scrollView должен быть ограничен краями ячейки. Затем вы установите ширину stackView в 2 раза ширину scrollView чтобы каждый вид контейнера имел ширину ячейки.

С помощью этой простой реализации вы создали базовую ячейку с видимым и скрытым видом. Используйте видимый вид для добавления содержимого в ячейку, а в скрытом виде вы можете добавить кнопку удаления. Таким образом, вы можете достичь этого:

swipe to delete

Я создал пример проекта на GitHub. Подробнее об этом решении вы также можете прочитать здесь.
Самое большое преимущество этого решения - простота и что вам не нужно иметь дело с ограничениями и распознавателями жестов.