ios - Cómo implementar el deslizamiento de UITableView para eliminar para UICollectionView
(6)
En la Guía de programación de Collection View para iOS , en la sección Incorporación de Gesture Support , los documentos leen:
Siempre debe adjuntar sus reconocedores de gestos a la vista de colección en sí y no a una celda o vista específica.
Por lo tanto, creo que no es una buena práctica agregar reconocedores a UICollectionViewCell
.
Solo me gustaría preguntar cómo puedo implementar el mismo comportamiento del barrido de UITableView para eliminar en UICollectionView. Estoy tratando de encontrar un tutorial pero no puedo encontrar ninguno.
Además, estoy usando el envoltorio PSTCollectionView para soportar iOS 5.
¡Gracias!
Editar: El reconocedor de deslizamiento ya es bueno. Lo que necesito ahora es la misma funcionalidad que UITableView cuando se cancela el modo Eliminar, por ejemplo, cuando el usuario toca una celda o en un espacio en blanco en la vista de tabla (es decir, cuando el usuario toca fuera del botón Eliminar). UITapGestureRecognizer no funcionará, ya que solo detecta pulsaciones al soltar un toque. UITableView detecta un toque en el inicio del gesto (y no en el lanzamiento), e inmediatamente cancela el modo Eliminar.
Es muy customContentView
customBackgroundView
agregar un customContentView
y customBackgroundView
detrás del customContentView
.
Después de eso, deberá cambiar el customContentView
a la izquierda mientras el usuario pasa de derecha a izquierda. Al cambiar la vista, se hace visible para customBackgroundView
.
Deja el código:
En primer lugar, debe agregar panGesture a su UICollectionView
como
override func viewDidLoad() {
super.viewDidLoad()
self.panGesture = UIPanGestureRecognizer(target: self, action: #selector(self.panThisCell))
panGesture.delegate = self
self.collectionView.addGestureRecognizer(panGesture)
}
Ahora implementa el selector como
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")
}
}
Métodos de ayuda para actualizar restricciones
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() }
})
}
He creado un proyecto de muestra here en Swift 3.
Es una versión modificada de este tutorial .
Existe una solución más estándar para implementar esta función, que tiene un comportamiento muy similar al proporcionado por UITableView
.
Para esto, utilizará un UIScrollView
como la vista raíz de la celda, y luego posicionará el contenido de la celda y el botón de eliminar dentro de la vista de desplazamiento. El código en tu clase de celular debería ser algo como esto:
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(scrollView)
scrollView.addSubview(viewWithCellContent)
scrollView.addSubview(deleteButton)
scrollView.isPagingEnabled = true
scrollView.showsHorizontalScrollIndicator = false
}
En este código, establecemos la propiedad isPagingEnabled
en true
para que la vista de desplazamiento deje de desplazarse solo en los límites de su contenido. Las subvistas de diseño para esta celda deberían ser algo como:
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)
}
Con este código en su lugar, si ejecuta la aplicación verá que el barrido para eliminar funciona como se esperaba, sin embargo, perdimos la capacidad de seleccionar la celda. El problema es que dado que la vista de desplazamiento está llenando toda la celda, todos los eventos táctiles son procesados por ella, por lo que la vista de colección nunca tendrá la oportunidad de seleccionar la celda (esto es similar a cuando tenemos un botón dentro de una celda, ya que los toques en ese botón no activan el proceso de selección, sino que se manejan directamente con el botón.)
Para solucionar este problema, solo tenemos que indicar la vista de desplazamiento para ignorar los eventos táctiles que procesa y no una de sus subvistas. Para lograr esto, simplemente cree una subclase de UIScrollView
y anule la siguiente función:
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
return result != self ? result : nil
}
Ahora, en su celda, debe usar una instancia de esta nueva subclase en lugar del UIScrollView
estándar.
Si ejecutas la aplicación ahora, verás que hemos recuperado la selección de celdas, pero esta vez el deslizamiento no funciona 😳. Ya que estamos ignorando los toques que son manejados directamente por la vista de desplazamiento, entonces su reconocedor de gesto de paneo no podrá comenzar a reconocer eventos táctiles. Sin embargo, esto puede solucionarse fácilmente indicando a la vista de desplazamiento que su reconocedor de gestos de paneo será manejado por la celda y no por el rollo. Para ello, agregue la siguiente línea en la parte inferior del init(frame: CGRect)
de su celda init(frame: CGRect)
:
addGestureRecognizer(scrollView.panGestureRecognizer)
Esto puede parecer un poco hacky, pero no lo es. Por diseño, la vista que contiene un reconocedor de gestos y el objetivo de ese reconocedor no tienen que ser el mismo objeto.
Después de este cambio, todo debería estar funcionando como se esperaba. Puedes ver una implementación completa de esta idea en este repositorio.
Existe una solución más sencilla para su problema que evita el uso de reconocedores de gestos. La solución se basa en UIScrollView
en combinación con UIStackView
.
Primero, debe crear 2 vistas de contenedor, una para la parte visible de la celda y otra para la parte oculta. Agregará estas vistas a un
UIStackView
. ElstackView
actuará como una vista de contenido. Asegúrese de que las vistas tengan los mismos anchos constackView.distribution = .fillEqually
.stackView
elstackView
dentro de unUIScrollView
que tiene habilitada la paginación. ElscrollView
debe estar restringido a los bordes de la celda. Luego, establecerá que elstackView
delstackView
sea 2 veces elscrollView
delscrollView
para que cada vista del contenedor tenga el ancho de la celda.
Con esta implementación simple, ha creado la celda base con una vista visible y oculta. Use la vista visible para agregar contenido a la celda y en la vista oculta puede agregar un botón de eliminación. De esta manera puedes lograr esto:
He configurado un proyecto de ejemplo en GitHub . También puede leer más sobre esta solución aquí .
La mayor ventaja de esta solución es la simplicidad y que no tiene que lidiar con restricciones y reconocedores de gestos.
Puedes intentar agregar un UISwipeGestureRecognizer a cada celda de colección, como esto:
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
CollectionViewCell *cell = ...
UISwipeGestureRecognizer* gestureRecognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(userDidSwipe:)];
[gestureRecognizer setDirection:UISwipeGestureRecognizerDirectionRight];
[cell addGestureRecognizer:gestureRecognizer];
}
seguido por:
- (void)userDidSwipe:(UIGestureRecognizer *)gestureRecognizer {
if (gestureRecognizer.state == UIGestureRecognizerStateEnded) {
//handle the gesture appropriately
}
}
Seguí un enfoque similar a @JacekLampart, pero decidí agregar UISwipeGestureRecognizer en la función awakeFromNib de UICollectionViewCell para que solo se agregue una vez.
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
}
}
En cuanto a salir del modo de eliminación, creé un UIGestureRecognizer personalizado con un NSArray de UIViews. Tomé prestada la idea de @iMS de esta pregunta: UITapGestureRecognizer : ¿hacer que funcione con un toque, no con un toque?
En touchesBegan, si el punto de contacto no está dentro de ninguna de las UIViews, el gesto tiene éxito y se sale del modo de eliminación.
De esta manera, puedo pasar el botón de eliminar dentro de la celda (y cualquier otra vista) al UIGestureRecognizer y, si el punto táctil está dentro del marco del botón, el modo de eliminación no se cerrará.
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
Implementación (en el UIViewController que contiene 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
}