Je voudrais juste demander comment puis-je mettre en œuvre le même comportement de glissement de UITableView à supprimer dans UICollectionView. J'essaie de trouver un tutoriel mais je n'en trouve pas.
De plus, j'utilise le wrapper PSTCollectionView pour prendre en charge iOS 5.
Je vous remercie!
Edit: La reconnaissance de balayage est déjà bonne. Ce dont j'ai besoin maintenant, c’est la même fonctionnalité que UITableView lors de l’annulation du mode Suppression, par exemple. lorsque l'utilisateur appuie sur une cellule ou sur un espace vide dans la vue du tableau (c'est-à-dire lorsqu'il appuie en dehors du bouton Supprimer). UITapGestureRecognizer ne fonctionnera pas, car il détecte uniquement les taps lors de la libération d'une touche. UITableView détecte le toucher au début du geste (et non au relâchement) et annule immédiatement le mode Supprimer.
Dans le Guide de programmation de la vue de collection pour iOS , dans la section Prise en charge intégrée des gestes , la documentation se lit comme suit:
Vous devez toujours attacher vos identificateurs de geste à la vue de collection elle-même et non à une cellule ou une vue spécifique.
Donc, je pense que ce n'est pas une bonne pratique d'ajouter des identifiants à UICollectionViewCell
.
C'est très simple. Vous devez ajouter customContentView
et customBackgroundView
derrière customContentView
.
Après cela, vous devez déplacer la customContentView
vers la gauche lorsque l’utilisateur glisse de droite à gauche. Décaler la vue rend visible la customBackgroundView
.
Lets Code:
Tout d’abord, vous devez ajouter panGesture à votre UICollectionView
en tant que
override func viewDidLoad() {
super.viewDidLoad()
self.panGesture = UIPanGestureRecognizer(target: self, action: #selector(self.panThisCell))
panGesture.delegate = self
self.collectionView.addGestureRecognizer(panGesture)
}
Maintenant, implémentez le sélecteur comme
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éthodes auxiliaires pour mettre à jour les contraintes
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() }
})
}
J'ai créé un exemple de projet ici dans Swift 3.
C'est une version modifiée de ce tutorial .
Il existe une solution plus simple à votre problème qui évite l’utilisation de dispositifs de reconnaissance de gestes. La solution est basée sur UIScrollView
en combinaison avec UIStackView
.
Tout d'abord, vous devez créer 2 vues de conteneur: une pour la partie visible de la cellule et une pour la partie cachée. Vous allez ajouter ces vues à une UIStackView
. La stackView
agira comme une vue de contenu. Assurez-vous que les vues ont la même largeur que stackView.distribution = .fillEqually
.
Vous incorporerez la stackView
dans une UIScrollView
pour laquelle la pagination est activée. La scrollView
doit être limitée aux bords de la cellule. Vous définissez ensuite la largeur de la stackView
sur 2 fois la largeur de la scrollView
afin que chacune des vues de conteneur ait la largeur de la cellule.
Avec cette implémentation simple, vous avez créé la cellule de base avec une vue visible et masquée. Utilisez la vue visible pour ajouter du contenu à la cellule et dans la vue masquée, vous pouvez ajouter un bouton de suppression. De cette façon, vous pouvez y parvenir:
J'ai mis en place un projet example sur GitHub . Vous pouvez également en savoir plus sur cette solution ici .
Le principal avantage de cette solution est sa simplicité et le fait de ne pas avoir à composer avec des contraintes et des dispositifs de reconnaissance des gestes.
J'ai suivi une approche similaire à @JacekLampart, mais j'ai décidé d'ajouter UISwipeGestureRecognizer dans la fonction awakeFromNib de UICollectionViewCell, de sorte qu'il ne l'a été ajouté qu'une seule fois.
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 ce qui concerne la sortie du mode Suppression, j'ai créé un UIGestureRecognizer personnalisé avec un NSArray of UIViews. J'ai emprunté l'idée de @iMS à cette question: UITapGestureRecognizer - faites-le fonctionner au toucher, pas à retoucher?
Sur touchesBegan, si le point de contact ne se trouve dans aucune des vues UIV, le geste aboutit et le mode de suppression est quitté.
De cette manière, je peux transmettre le bouton de suppression de la cellule (et toutes les autres vues) à UIGestureRecognizer et, si le point de contact se trouve dans le cadre du bouton, le mode de suppression ne se fermera pas.
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
Implémentation (dans UIViewController contenant 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
}
Vous pouvez essayer d'ajouter un identificateur UISwipeGestureRecognizer à chaque cellule de collection, comme suit:
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
CollectionViewCell *cell = ...
UISwipeGestureRecognizer* gestureRecognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(userDidSwipe:)];
[gestureRecognizer setDirection:UISwipeGestureRecognizerDirectionRight];
[cell addGestureRecognizer:gestureRecognizer];
}
suivi par:
- (void)userDidSwipe:(UIGestureRecognizer *)gestureRecognizer {
if (gestureRecognizer.state == UIGestureRecognizerStateEnded) {
//handle the gesture appropriately
}
}
Il existe une solution plus standard pour implémenter cette fonctionnalité, avec un comportement très similaire à celui fourni par UITableView
.
Pour cela, vous utiliserez une UIScrollView
comme vue racine de la cellule, puis positionnez le contenu de la cellule et le bouton de suppression dans la vue de défilement. Le code de votre classe de cellules devrait ressembler à ceci:
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(scrollView)
scrollView.addSubview(viewWithCellContent)
scrollView.addSubview(deleteButton)
scrollView.isPagingEnabled = true
scrollView.showsHorizontalScrollIndicator = false
}
Dans ce code, nous avons défini la propriété isPagingEnabled
sur true
pour que la vue de défilement cesse de défiler uniquement aux limites de son contenu. Les sous-vues de présentation de cette cellule doivent ressembler à ceci:
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)
}
Avec ce code en place, si vous exécutez l'application, vous constaterez que le balayage à supprimer fonctionne comme prévu. Toutefois, nous avons perdu la possibilité de sélectionner la cellule. Le problème est que, comme la vue de défilement remplit toute la cellule, tous les événements tactiles sont traités par elle. La vue de collecte n'aura donc jamais la possibilité de sélectionner la cellule. étant donné que des pressions sur ce bouton ne déclenchent pas le processus de sélection, elles sont gérées directement par le bouton.)
Pour résoudre ce problème, il suffit d'indiquer que la vue de défilement ignore les événements tactiles traités par cette dernière et non par l'une de ses sous-vues. Pour ce faire, créez simplement une sous-classe de UIScrollView
et remplacez la fonction suivante:
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
return result != self ? result : nil
}
Maintenant, dans votre cellule, vous devez utiliser une instance de cette nouvelle sous-classe au lieu de la variable standard UIScrollView
.
Si vous exécutez l'application maintenant, vous constaterez que la sélection de cellule a été rétablie, mais cette fois, le balayage ne fonctionne pas ???? Étant donné que nous ignorons les touchers gérés directement par la vue de défilement, son outil de reconnaissance des gestes panoramiques ne pourra pas commencer à reconnaître les événements tactiles. Cependant, ceci peut être facilement corrigé en indiquant à la vue de défilement que sa reconnaissance de gestes de panoramique sera gérée par la cellule et non par le défilement. Vous faites cela en ajoutant la ligne suivante au bas de la fonction init(frame: CGRect)
de votre cellule:
addGestureRecognizer(scrollView.panGestureRecognizer)
Cela peut sembler un peu hacky, mais ce n'est pas le cas. De par sa conception, la vue contenant un identificateur de geste et la cible de cet identificateur ne doivent pas nécessairement être le même objet.
Après ce changement, tout devrait fonctionner comme prévu. Vous pouvez voir une mise en œuvre complète de cette idée dans ce dépôt