skip to Main Content

I ran into a problem when using the simplest UICollectionView and UICollectionViewFlowLayout.

The collection itself works fine, but when the cell is removed, there are problems with the animation.

Here is a code example that demonstrates the problem:

class Cell: UICollectionViewCell { }

class MyViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
    var items = Array(repeating: [UIColor.red, .blue, .yellow, .cyan, .magenta, .green], count: 100).flatMap { $0 }
    
    lazy var collectionView: UICollectionView = {
        let collectionViewLayout = UICollectionViewFlowLayout()
        collectionViewLayout.scrollDirection = .vertical
        collectionViewLayout.minimumLineSpacing = 10
        collectionViewLayout.minimumInteritemSpacing = 10
        
        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        collectionView.register(Cell.self, forCellWithReuseIdentifier: "Cell")
        collectionView.delegate = self
        collectionView.dataSource = self
        return collectionView
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(collectionView)
        NSLayoutConstraint.activate([
            .init(item: collectionView, attribute: .leading, relatedBy: .equal, toItem: view, attribute: .leading, multiplier: 1, constant: 0),
            .init(item: collectionView, attribute: .trailing, relatedBy: .equal, toItem: view, attribute: .trailing, multiplier: 1, constant: 0),
            .init(item: collectionView, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: .bottom, multiplier: 1, constant: 0),
            .init(item: collectionView, attribute: .top, relatedBy: .equal, toItem: view, attribute: .top, multiplier: 1, constant: 0),
        ])
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        items.count
    }
        
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! Cell
        cell.backgroundColor = items[indexPath.item]
        return cell
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        CGSize(width: UIScreen.main.bounds.width, height: 300)
    }
    
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        items.remove(at: indexPath.item)
        collectionView.deleteItems(at: [indexPath])
    }
}

The problem itself looks like this (in slow motion):

You can notice how the next cell disappears for the duration of the animation, and at the end of it it appears.

Question: what am I doing wrong? This is a very basic use of a collection and I’m confused by these issues.

3

Answers


  1. Chosen as BEST ANSWER

    From the rest of the answers, the reason for this glitch became clear - there are problems with the deletion animation if some of the new cells are off the screen.

    I solved the problem by adding layout invalidation to the animation block:

    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        collectionView.performBatchUpdates({
            self.items.remove(at: indexPath.item)
            collectionView.deleteItems(at: [indexPath])
            collectionView.collectionViewLayout.invalidateLayout()
        }, completion: nil)
    }
    

  2. It seems like there is a problem with your array.
    It’s created dynamically and has conflicts with UICollectionView’s implementation.

    Try replacing your array with the following static array.

        var items = [UIColor.red, .blue, .yellow, .cyan, .magenta, .green, .red, .blue, .yellow, .cyan, .magenta, .green]
    

    Here is what it looks like after changing it.

    enter image description here

    Login or Signup to reply.
  3. You have to subclass UICollectionViewFlowLayout and override these two methods
    initialLayoutAttributesForAppearingItem
    finalLayoutAttributesForDisappearingItem

    by changing "attributes" properties you will get different effects just change the line 59 animation duration and check them out.

    import UIKit
    
    class Cell: UICollectionViewCell { }
    
    class ViewController: UIViewController ,UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
        
        var items = Array(repeating: [UIColor.red, .blue, .yellow, .cyan, .magenta, .green], count: 100).flatMap { $0 }
        
        lazy var collectionView: UICollectionView = {
            
            let collectionViewLayout = MyCollectionLayout()
            collectionViewLayout.scrollDirection = .vertical
            collectionViewLayout.minimumLineSpacing = 10
            collectionViewLayout.minimumInteritemSpacing = 10
            
            let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
            collectionView.translatesAutoresizingMaskIntoConstraints = false
            collectionView.register(Cell.self, forCellWithReuseIdentifier: "Cell")
            collectionView.delegate = self
            collectionView.dataSource = self
            return collectionView
        }()
        
        override func viewDidLoad() {
            super.viewDidLoad()
            view.addSubview(collectionView)
            NSLayoutConstraint.activate([
                .init(item: collectionView, attribute: .leading, relatedBy: .equal, toItem: view, attribute: .leading, multiplier: 1, constant: 0),
                .init(item: collectionView, attribute: .trailing, relatedBy: .equal, toItem: view, attribute: .trailing, multiplier: 1, constant: 0),
                .init(item: collectionView, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: .bottom, multiplier: 1, constant: 0),
                .init(item: collectionView, attribute: .top, relatedBy: .equal, toItem: view, attribute: .top, multiplier: 1, constant: 0),
            ])
        }
        
        func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
            items.count
        }
        
        func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! Cell
            cell.backgroundColor = items[indexPath.item]
            return cell
        }
        
        func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
            CGSize(width: UIScreen.main.bounds.width, height: 300)
        }
        
        
        func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
            
            UIView.animate(withDuration: 0.2) {
                self.collectionView.performBatchUpdates({
                    
                    self.items.remove(at: indexPath.item)
                    collectionView.deleteItems(at: [indexPath])
                }, completion: nil)
            }
            
        }
    }
    
    
    
    class  MyCollectionLayout : UICollectionViewFlowLayout {
        var rowOffset : CGFloat = 0.0
        var deleteIndexPaths : Array<IndexPath> = []
        var insertIndexPaths : Array<IndexPath> = []
        
        override func prepare(forCollectionViewUpdates updateItems: [UICollectionViewUpdateItem]) {
            super.prepare(forCollectionViewUpdates: updateItems)
            
            self.deleteIndexPaths =  Array<IndexPath>()
            self.insertIndexPaths =  Array<IndexPath>()
            
            for updateItem in updateItems {
                if (updateItem.updateAction == .delete){
                    self.deleteIndexPaths.append(updateItem.indexPathBeforeUpdate!)
                    
                }else if (updateItem.updateAction == .insert){
                    self.insertIndexPaths.append(updateItem.indexPathAfterUpdate!)
                }
            }
        }
        
        
        override func initialLayoutAttributesForAppearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
            var attributes = super.initialLayoutAttributesForAppearingItem(at: itemIndexPath)
            if(attributes == nil){
                attributes = self.layoutAttributesForItem(at: itemIndexPath)
            }
            
            attributes?.alpha = 1
            return attributes
        }
        
        override func finalizeCollectionViewUpdates()
        {
            super.finalizeCollectionViewUpdates()
            self.deleteIndexPaths = [];
            self.insertIndexPaths = [];
        }
        
        
        override func finalLayoutAttributesForDisappearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
            
            var attributes = super.finalLayoutAttributesForDisappearingItem(at: itemIndexPath)
            if ((attributes == nil)){
                attributes = self.layoutAttributesForItem(at: itemIndexPath)
            }
            attributes?.transform = CGAffineTransform(scaleX: 0, y: 0)
            attributes?.zIndex = -1
            attributes?.alpha = 1
            
            return attributes
        }
    }
    

    enter image description here

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search