web-dev-qa-db-fra.com

Sélection de cellules UICollectionView et réutilisation de cellules

Lors de la sélection de la cellule, je souhaite gérer le changement de son apparence. J'ai pensé que la méthode de délégation collectionView:didSelectItemAtIndexPath: & collectionView:didDeselectItemAtIndexPath: est l'endroit où je devrais modifier la cellule.

-(void)collectionView:(UICollectionView *)collectionView 
       didSelectItemAtIndexPath:(NSIndexPath *)indexPath {

    DatasetCell *datasetCell = 
      (DatasetCell *)[collectionView cellForItemAtIndexPath:indexPath];

    [datasetCell replaceHeaderGradientWith:[UIColor skyBlueHeaderGradient]];
    datasetCell.backgroundColor = [UIColor skyBlueColor];
}

et

-(void)collectionView:(UICollectionView *)collectionView 
       didDeselectItemAtIndexPath:(NSIndexPath *)indexPath {

    DatasetCell *datasetCell = 
      (DatasetCell *)[collectionView cellForItemAtIndexPath:indexPath];

    [datasetCell replaceHeaderGradientWith:[UIColor grayGradient]];
    datasetCell.backgroundColor = [UIColor myDarkGrayColor];
}

Cela fonctionne bien, sauf lorsque la cellule est réutilisée. Si je sélectionne la cellule à l'index (0, 0), cela change l'apparence, mais lorsque je fais défiler l'écran vers le bas, il y a une autre cellule dans l'état sélectionné.

Je crois que je devrais utiliser la méthode UICollectionViewCell-(void)prepareForReuse pour préparer la cellule à la réémission (c.-à-d. Définir l'apparence de la cellule sur un état non sélectionné), mais cela me pose des difficultés.

-(void)prepareForReuse {
    if ( self.selected ) {
        [self replaceHeaderGradientWith:[UIColor skyBlueHeaderGradient]];
        self.backgroundColor = [UIColor skyBlueColor];
    } else {
        [self replaceHeaderGradientWith:[UIColor grayGradient]];
        self.backgroundColor = [UIColor myDarkGrayColor];
    }
}

Lorsque je reviens en haut de la page, la cellule à l'index (0, 0) est dans l'état désélectionné.

Lorsque je viens d'utiliser la propriété cell.backgroundView, pour éviter cela, il fallait:

-(void)prepareForReuse {
    self.selected = FALSE;
}

et l'état de sélection a fonctionné comme prévu.

Des idées?

45
Padin215

Votre observation est correcte. Ce comportement est dû à la réutilisation de cellules. Mais vous n'avez rien à faire avec le prepareForReuse . Faites plutôt votre vérification dans cellForItem et définissez les propriétés en conséquence. Quelque chose comme.. 

 - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"cvCell" forIndexPath:indexPath];


if (cell.selected) {
     cell.backgroundColor = [UIColor blueColor]; // highlight selection 
}
else
{
     cell.backgroundColor = [UIColor redColor]; // Default color
}
return cell;
}

-(void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath  {

    UICollectionViewCell *datasetCell =[collectionView cellForItemAtIndexPath:indexPath];
    datasetCell.backgroundColor = [UIColor blueColor]; // highlight selection
 }  

 -(void)collectionView:(UICollectionView *)collectionView didDeselectItemAtIndexPath:(NSIndexPath *)indexPath {

UICollectionViewCell *datasetCell =[collectionView cellForItemAtIndexPath:indexPath]; 
datasetCell.backgroundColor = [UIColor redColor]; // Default color
}
70
Anil Varghese

Framework gérera la commutation des vues pour vous une fois que vous aurez configuré les variables backgroundView et selectedBackgroundView de votre cellule, voir l'exemple de Gestion de l'état visuel des sélections et des points forts :

UIView* backgroundView = [[UIView alloc] initWithFrame:self.bounds];
backgroundView.backgroundColor = [UIColor redColor];
self.backgroundView = backgroundView;

UIView* selectedBGView = [[UIView alloc] initWithFrame:self.bounds];
selectedBGView.backgroundColor = [UIColor whiteColor];
self.selectedBackgroundView = selectedBGView;

vous avez seulement besoin dans votre classe qui implémente UICollectionViewDelegate permettre aux cellules d'être mises en surbrillance et sélectionnées comme ceci:

- (BOOL)collectionView:(UICollectionView *)collectionView
        shouldHighlightItemAtIndexPath:(NSIndexPath *)indexPath
{
    return YES;
}

- (BOOL)collectionView:(UICollectionView *)collectionView
        shouldSelectItemAtIndexPath:(NSIndexPath *)indexPath;
{
    return YES;
}

Cela me fonctionne.

23
stefanB

UICollectionView a changé dans iOS 10 en introduisant certains problèmes dans les solutions ci-dessus.

Voici un bon guide: https://littlebitesofcocoa.com/241-uicollectionview-cell-pre-fetching

Les cellules restent un peu après avoir quitté l'écran. Ce qui signifie que nous ne pouvons parfois pas obtenir une cellule dans didDeselectItemAt indexPath afin de l'ajuster. Il peut ensuite apparaître sur l'écran non mis à jour et non recyclé. prepareForReuse n'aide pas ce cas de coin.

La solution la plus simple consiste à désactiver le nouveau défilement en définissant isPrefetchingEnabled sur false. Avec cela, gérer l'affichage de la cellule avec cellForItemAtdidSelectdidDeselect fonctionne comme avant.

Toutefois, si vous préférez conserver le nouveau comportement de défilement régulier, utilisez plutôt willDisplay:

func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
    let customCell = cell as! CustomCell
    if customCell.isSelected {
        customCell.select()
    } else {
        customCell.unselect()
    }
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CustomCell", for: indexPath) as! CustomCell
    //Don't even need to set selection-specific things here as recycled cells will also go through willDisplay
    return cell
}

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    let cell = collectionView.cellForItem(at: indexPath) as? CustomCell
    cell?.select()
}

func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
    let cell = collectionView.cellForItem(at: indexPath) as? CustomCell
    cell?.unselect() // <----- this can be null here, and the cell can still come back on screen!
}

Avec ce qui précède, vous contrôlez la cellule lorsqu'elle est sélectionnée, désélectionnée à l'écran, recyclée et juste affichée à nouveau.

19

Anil était sur la bonne voie (sa solution semble fonctionner, j'ai développé cette solution indépendamment de la sienne). J'ai toujours utilisé la méthode prepareForReuse: pour définir la variable selected de la cellule sur FALSE, puis dans la variable cellForItemAtIndexPath, je vérifie si l'index de la cellule se trouve dans `collectionView.indexPathsForSelectedItems ', si c'est le cas, mettez-le en surbrillance.

Dans la cellule personnalisée:

-(void)prepareForReuse {
    self.selected = FALSE;
}

Dans cellForItemAtIndexPath: pour gérer la mise en surbrillance et la mise en évidence des cellules de réutilisation:

if ([collectionView.indexPathsForSelectedItems containsObject:indexPath]) {
    [collectionView selectItemAtIndexPath:indexPath animated:FALSE scrollPosition:UICollectionViewScrollPositionNone];
    // Select Cell
}
else {
    // Set cell to non-highlight
}

Et puis gérer la mise en surbrillance et la surbrillance des cellules dans les didDeselectItemAtIndexPath: et didSelectItemAtIndexPath:

Cela fonctionne comme un charme pour moi.

10
Padin215

J'avais une vue de collection à défilement horizontal (j'utilise la vue de collection dans Tableview) et j'ai également rencontré des problèmes de réutilisation des cellules. Chaque fois que je sélectionne un élément et que je défile vers la droite, certaines autres cellules de l'ensemble visible suivant sont automatiquement sélectionnées. Essayer de résoudre ce problème en utilisant des propriétés de cellule personnalisées telles que "sélectionné", en surbrillance, etc. ne m'a pas aidé. J'ai donc proposé la solution ci-dessous et cela a fonctionné pour moi.

Étape 1:

Créez une variable dans collectionView pour stocker l'index sélectionné. J'ai utilisé ici une variable de niveau de classe appelée selectedIndex. 

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath

{

    MyCVCell *cell = (MyCVCell*)[collectionView dequeueReusableCellWithReuseIdentifier:@"MyCVCell" forIndexPath:indexPath];    

// When scrolling happens, set the selection status only if the index matches the selected Index

if (selectedIndex == indexPath.row) {

        cell.layer.borderWidth = 1.0;

        cell.layer.borderColor = [[UIColor redColor] CGColor];

    }
    else
    {
        // Turn off the selection
        cell.layer.borderWidth = 0.0;

    }
    return cell;

}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath

{
    MyCVCell *cell = (MyCVCell *)[collectionView cellForItemAtIndexPath:indexPath];
    // Set the index once user taps on a cell
    selectedIndex = indexPath.row;
    // Set the selection here so that selection of cell is shown to ur user immediately
    cell.layer.borderWidth = 1.0;
    cell.layer.borderColor = [[UIColor redColor] CGColor];
    [cell setNeedsDisplay];
}

- (void)collectionView:(UICollectionView *)collectionView didDeselectItemAtIndexPath:(NSIndexPath *)indexPath

{

    MyCVCell *cell = (MyCVCell *)[collectionView cellForItemAtIndexPath:indexPath];

    // Set the index to an invalid value so that the cells get deselected
    selectedIndex = -1;
    cell.layer.borderWidth = 0.0;
    [cell setNeedsDisplay];

}

-anoop

6
anoop4real

Dans votre cellule personnalisée, créez une méthode publique:

- (void)showSelection:(BOOL)selection
{
    self.contentView.backgroundColor = selection ? [UIColor blueColor] : [UIColor white];
}

Également écrire la redéfinition de la méthode de la cellule -prepareForReuse:

- (void)prepareForReuse
{
    [self showSelection:NO];
    [super prepareForReuse];
}

Et dans votre ViewController, vous devriez avoir la variable _selectedIndexPath, définie dans -didSelectItemAtIndexPath et nulle dans -didDeselectItemAtIndexPath

NSIndexPath *_selectedIndexPath;

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *cellIdentifier = @"Cell";
    UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:cellIdentifier forIndexPath:indexPath];

    if (_selectedIndexPath) {
        [cell showSelection:[indexPath isEqual:_selectedIndexPath]];
    }
}

- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
    UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
    [cell showSelection:![indexPath isEqual:_selectedIndexPath]];// on/off selection
    _selectedIndexPath = [indexPath isEqual:_selectedIndexPath] ? nil : indexPath;
}

- (void)collectionView:(UICollectionView *)collectionView didDeselectItemAtIndexPath:(NSIndexPath *)indexPath
{
    UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
    [cell showSelection:NO];
    _selectedIndexPath = nil;
}
2
landonandrey

Ce que j'ai fait pour résoudre ce problème a été d'apporter les modifications dans la cellule personnalisée. Vous avez une cellule personnalisée appelée DataSetCell dans sa classe, vous pouvez effectuer les opérations suivantes (le code est dans Swift)

override var isSelected: Bool {
    didSet {
        if isSelected {
            changeStuff
        } else {
            changeOtherStuff
        }
    }
}

Cela a pour effet que chaque fois que la cellule est sélectionnée, désélectionnée, initialisée ou appelée à partir de la file d'attente réutilisable, ce code s'exécute et les modifications sont apportées. J'espère que cela vous aide.

1
Eliezer Ferra

Le problème que vous rencontrez provient de l'absence d'appel à super.prepareForReuse().

Certaines des solutions ci-dessus, suggérant d'actualiser l'interface utilisateur de la cellule à partir des fonctions du délégué, conduisent à une conception erronée dans laquelle la logique du comportement de la cellule est en dehors de sa classe. De plus, c'est un code supplémentaire qui peut être simplement corrigé en appelant super.prepareForReuse(). Par exemple :

class myCell: UICollectionViewCell {

    // defined in interface builder
    @IBOutlet weak var viewSelection : UIView!

    override var isSelected: Bool {
        didSet {
            self.viewSelection.alpha = isSelected ? 1 : 0
        }
    }

    override func prepareForReuse() {
        // Do whatever you want here, but don't forget this :
        super.prepareForReuse()
        // You don't need to do `self.viewSelection.alpha = 0` here 
        // because `super.prepareForReuse()` will update the property `isSelected`

    }


    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
        self.viewSelection.alpha = 0
    }

}

Avec une telle conception, vous pouvez même laisser les fonctions du délégué collectionView:didSelectItemAt:/collectionView:didDeselectItemAt: toutes vides, et le processus de sélection sera totalement géré et se comportera correctement avec le recyclage des cellules.

Seule la solution @ stefanB a fonctionné pour moi sur iOS 9.3

Voici ce que je dois changer pour Swift 2

func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {

        //prepare your cell here..

        //Add background view for normal cell
        let backgroundView: UIView = UIView(frame: cell!.bounds)
        backgroundView.backgroundColor = UIColor.lightGrayColor()
        cell!.backgroundView = backgroundView

        //Add background view for selected cell
        let selectedBGView: UIView = UIView(frame: cell!.bounds)
        selectedBGView.backgroundColor = UIColor.redColor()
        cell!.selectedBackgroundView = selectedBGView

        return cell!
 }

 func collectionView(collectionView: UICollectionView, shouldHighlightItemAtIndexPath indexPath: NSIndexPath) -> Bool {
        return true
 }

 func collectionView(collectionView: UICollectionView, shouldSelectItemAtIndexPath indexPath: NSIndexPath) -> Bool {
        return true
 }
1
swiftBoy

La modification de la propriété de la cellule, telle que les couleurs d'arrière-plan de celle-ci, ne doit pas être effectuée sur le UICollectionViewController lui-même. Elle doit être effectuée dans votre classe CollectionViewCell. N'utilisez pas didSelect et didDeselect, utilisez simplement ceci:

class MyCollectionViewCell: UICollectionViewCell 
{
     override var isSelected: Bool
     {
         didSet
         {
            // Your code
         }
     } 
}
0
pierre23

vous pouvez simplement définir la propriété selectedBackgroundView de la cellule sur backgroundColor = x.

Désormais, chaque fois que vous appuyez sur une cellule, son mode sélectionné change automatiquement et renvoie à la couleur d'arrière-plan pour devenir x.

0
evya

Merci à votre réponse @ _ RDC .

Les codes suivants fonctionnent avec Swift 3

// MARK: - UICollectionViewDataSource protocol
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

    //prepare your cell here..
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath as IndexPath) as! MyCell
    cell.myLabel.text =  "my text"

    //Add background view for normal cell
    let backgroundView: UIView = UIView(frame: cell.bounds)
    backgroundView.backgroundColor = UIColor.lightGray
    cell.backgroundView = backgroundView

    //Add background view for selected cell
    let selectedBGView: UIView = UIView(frame: cell.bounds)
    selectedBGView.backgroundColor = UIColor.green
    cell.selectedBackgroundView = selectedBGView

    return cell
}

// MARK: - UICollectionViewDelegate protocol
func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool {
    return true
}

func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
    return true
}
0
Ben