Home > other >  How can I force animations in UITableViewCells to stop while scrolling through the table view?
How can I force animations in UITableViewCells to stop while scrolling through the table view?

Time:11-23

The problem I face itself is simple to describe.

I have a table view in my application in which animations are started asynchronously. And for once index path, the animations happen only once. If I scroll the cell out of visible area, I assume that the animation for that index path are finished. i.e. Next time the index path comes back to the visible area, I set it to the final values without animation. If I do not scroll, the animations work as expected.

However, when I scroll the table view, because the cell is reused and the animations intended to be performed on the cells currently not visible are still being performed on the now visible cell. I have put together a sample code that describes my problem.

import UIKit

class ViewController: UIViewController {
    
    @IBOutlet weak var tableView: UITableView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.dataSource = self
    }
}


extension ViewController: UITableViewDataSource, UITableViewDelegate {
    
    func numberOfSections(in tableView: UITableView) -> Int {
        1
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        100
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "TestCell", for: indexPath)
        cell.textLabel?.layer.opacity = 1
        UIView.animate(withDuration: 2, delay: 0, options: .curveEaseIn) { [weak cell] in
            cell?.textLabel?.layer.opacity = 0
        } completion: { [weak cell] _ in
            cell?.textLabel?.layer.opacity = 0
            cell?.textLabel?.text = "\(indexPath.row)"
            UIView.animate(withDuration: 2, delay: 0, options: .curveEaseIn) { [weak cell] in
                cell?.textLabel?.layer.opacity = 1
            } completion: { [weak cell] _ in
                cell?.textLabel?.layer.opacity = 1
                cell?.textLabel?.text = "\(indexPath.row)"
            }
        }
        animated[indexPath.row].toggle()
        return cell
    }
}

To stop the animation if the cell goes out of bounds of the table view, I tried to save a reference of the animation in the cell and remove right after a cell is dequeued. But it doesn't help my case.

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var tableView: UITableView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.dataSource = self
    }
    
    var animated: [Bool] = Array(repeating: false, count: 100)
}

class TestCell: UITableViewCell {
    
    var animations: [UIViewPropertyAnimator] = []
}

extension ViewController: UITableViewDataSource, UITableViewDelegate {
    
    func numberOfSections(in tableView: UITableView) -> Int {
        1
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        100
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "TestCell", for: indexPath) as! TestCell
        
        if !animated[indexPath.row] {
            
            //Stop animations if any
            cell.animations.forEach { animator in
                animator.stopAnimation(true)
            }
            cell.animations.removeAll()

            //
            let animator = UIViewPropertyAnimator(duration: 5, curve: .easeIn) { [weak cell] in
                cell?.textLabel?.layer.opacity = 0
            }
            
            animator.addCompletion { [weak cell] completed in
                cell?.textLabel?.layer.opacity = 0
                cell?.textLabel?.text = "\(indexPath.row)"
                
                if completed == .end {
                    
                    let animator1 = UIViewPropertyAnimator(duration: 5, curve: .easeIn) { [weak cell] in
                        cell?.textLabel?.layer.opacity = 1
                    }
                    
                    animator1.addCompletion { [weak cell] _ in
                        cell?.textLabel?.layer.opacity = 1
                        cell?.textLabel?.text = "\(indexPath.row)"
                    }
                    
                    animator1.startAnimation(afterDelay: 0)
                    cell?.animations.append(animator1)
                }
            }
            
            animator.startAnimation(afterDelay: 0)
            cell.animations.append(animator)
            
            animated[indexPath.row].toggle()
        } else {
            cell.textLabel?.layer.opacity = 1
            cell.textLabel?.text = "\(indexPath.row)"
        }
        return cell
    }
}

I could not find anything similar on SO. Any help is appreciated.

CodePudding user response:

Set/stop animation in cell class :

class TestCell: UITableViewCell {

    var animation : UIViewPropertyAnimator?

    // call this in cellForRow in tableView controller 
    func startAnimation() {
         create animation and save it in self.animation
    }

    func stopAnimation () {
         stop animation 
    }

    override func prepareForReuse() {
        super.prepareForReuse()
        StopAnimation();
    }
}

CodePudding user response:

It's a bit of an odd animation sequence, but I'm guessing this is more for testing / figuring it out for your actual use.

This might help...

Implement prepareForReuse() in your cell to stop and remove any executing animations, and set the label's opacity to 1.0.

In cellForRowAt, set the slot in your animated array to false instead of toggling it, since you only want the animation to run the first time a row appears.

Also in cellForRowAt, make sure you "reset" the cell's label to its initial value.

Give this a try and see if it gets you closer to your goal:

class TestCell: UITableViewCell {
    
    var animations: [UIViewPropertyAnimator] = []
    
    override func prepareForReuse() {
        super.prepareForReuse()
        
        animations.forEach { animator in
            animator.stopAnimation(true)
        }
        animations.removeAll()

        textLabel?.layer.opacity = 1.0
    }
}

class ViewController: UIViewController {
    
    @IBOutlet weak var tableView: UITableView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.dataSource = self
    }
    
    var animated: [Bool] = Array(repeating: false, count: 100)
}
extension ViewController: UITableViewDataSource, UITableViewDelegate {
    
    func numberOfSections(in tableView: UITableView) -> Int {
        1
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        100
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "TestCell", for: indexPath) as! TestCell
        
        if !animated[indexPath.row] {
            
            animated[indexPath.row] = true
            
            // cells are reused, so "reset" the initial label text
            cell.textLabel?.text = "Needs to animate..."

            //
            let animator = UIViewPropertyAnimator(duration: 5, curve: .easeIn) { [weak cell] in
                cell?.textLabel?.layer.opacity = 0
            }
            
            animator.addCompletion { [weak cell] completed in
                cell?.textLabel?.layer.opacity = 0
                cell?.textLabel?.text = "\(indexPath.row)"
                
                if completed == .end {
                    
                    let animator1 = UIViewPropertyAnimator(duration: 5, curve: .easeIn) { [weak cell] in
                        cell?.textLabel?.layer.opacity = 1
                    }
                    
                    animator1.addCompletion { [weak cell] _ in
                        cell?.textLabel?.layer.opacity = 1
                        cell?.textLabel?.text = "\(indexPath.row)"
                    }
                    
                    animator1.startAnimation(afterDelay: 0)
                    cell?.animations.append(animator1)
                }
            }
            
            animator.startAnimation(afterDelay: 0)
            cell.animations.append(animator)
            
        } else {
            cell.textLabel?.layer.opacity = 1
            cell.textLabel?.text = "\(indexPath.row)"
        }
        return cell
    }
    
}
  • Related