Как реализовать прокрутку 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
.
-
Во-первых, вам нужно создать 2 вида контейнера - один для видимой части ячейки и один для скрытой части. Вы добавите эти представления в UIStackView
. stackView
будет действовать как просмотр содержимого. Убедитесь, что представления имеют равную ширину с помощью stackView.distribution =.fillEqually
.
-
Youll внедряет stackView
внутри UIScrollView
с включенным пейджингом. scrollView
должен быть ограничен краями ячейки. Затем вы установите ширину stackView
в 2 раза ширину scrollView
чтобы каждый вид контейнера имел ширину ячейки.
С помощью этой простой реализации вы создали базовую ячейку с видимым и скрытым видом. Используйте видимый вид для добавления содержимого в ячейку, а в скрытом виде вы можете добавить кнопку удаления. Таким образом, вы можете достичь этого:
![swipe to delete]()
Я создал пример проекта на GitHub. Подробнее об этом решении вы также можете прочитать здесь.
Самое большое преимущество этого решения - простота и что вам не нужно иметь дело с ограничениями и распознавателями жестов.