Woodstock
Woodstock

Reputation: 22926

Timing issue when animating custom UITableViewCell

I'm having an issue with my application architecture...

I have a UITableView filled with custom UITableViewCells. Of course, I use dequeuing so there are only 8-10 cell instances ever generated.

Moving forward to my problem... I have added a theming feature to my application. When the user long touches the main UINavigationBara notification is posted app wide that informs each viewController to update their UI.

When the viewController hosting the tableView with the custom cells receives the notification, it calls tableView.reloadData()

This works well, as I have implemented func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath)

Inside willDisplay I call a method on my custom tableViewCell which animates, using UIViewAnimation, appropriate colour changes to the cell.

It looks like this:

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    if let newsFeedItemCell = cell as? NewsFeedItemCell {
        if (self.nightModeEnabled) {
            newsFeedItemCell.transitionToNightMode()
        } else {
            newsFeedItemCell.transitionFromNightMode()
        }
    }
}

Inside the custom cell implementation those methods look like this:

func transitionToNightMode() {
        UIView.animate(withDuration: 1, animations: {
            self.backgroundColor = UIColor.black
            self.titleTextView.backgroundColor = UIColor.black
            self.titleTextView.textColor = UIColor.white
        })
}

func transitionFromNightMode() {
    UIView.animate(withDuration: 1, animations: {
        self.backgroundColor = UIColor.white
        self.titleTextView.backgroundColor = UIColor.white
        self.titleTextView.textColor = UIColor.black
    })
}

This works fine... heres the issue:

Upon scrolling the tableView, any cells that weren't on screen have their colour update/animation code called as they scroll onto the screen, which leads to a jarring user experience.

I understand why this is happening of course, as willDisplay is only called as cells display.

I can't think of an elegant way to avoid this.

I'm happy that cells on screen are animating for the user experience to be pleasant, however, for cells off screen, I'd rather they skipped the animation.


Possible solutions (though inelegant):

Keep a reference to each of the 8-10 cells created by cellForRow, and check if they are off screen, if they are set their state immediately.

However, I don't like the idea of keeping a reference to each cell.

Any ideas?

Upvotes: 1

Views: 400

Answers (2)

disconnectionist
disconnectionist

Reputation: 508

I would not use a reloadData to animate this, since your data model for the tableview is not actually changing.

Instead, I would give the UITableView a function, like so:

class myClass : UITableViewController {
    ....

    func transitionToNightMode() {
            for visible in visibleCells {
            UIView.animate(withDuration: 1, animations: {
                visible.backgroundColor = UIColor.black
                visible.titleTextView.backgroundColor = UIColor.black
                visible.titleTextView.textColor = UIColor.white
            })
            }
    }
}

and then in your willDisplay or cellForItemAt, set the correct appearance without animation.

Upvotes: 3

Milan Nosáľ
Milan Nosáľ

Reputation: 19737

I would try the following.

Instead of using UIView.animate, in transition(To/From)NightMode I would create a UIViewPropertyAnimator object that would do the same animation. I would keep the reference to that object around and then in prepare for reuse, if the animation is still running, I would simply finish that animation and reset the state. So something like this:

fileprivate var transitionAnimator: UIViewPropertyAnimator?

fileprivate func finishAnimatorAnimation() {
    // if there is a transitionAnimator, immediately finishes it
    if let animator = transitionAnimator {
        animator.stopAnimation(false)
        animator.finishAnimation(at: .end)
        transitionAnimator = nil
    }
}

override func prepareForReuse() {
    super.prepareForReuse()
    finishAnimatorAnimation()
}

func transitionToNightMode() {
    transitionAnimator = UIViewPropertyAnimator(duration: 1, curve: .easeInOut, animations: {
        self.backgroundColor = UIColor.black
        self.titleTextView.backgroundColor = UIColor.black
        self.titleTextView.textColor = UIColor.white
    })
    transitionAnimator?.startAnimation()
}

func transitionFromNightMode() {
    transitionAnimator = UIViewPropertyAnimator(duration: 1, curve: .easeInOut, animations: {
        self.backgroundColor = UIColor.white
        self.titleTextView.backgroundColor = UIColor.white
        self.titleTextView.textColor = UIColor.black
    })
    transitionAnimator?.startAnimation()
}

Upvotes: 1

Related Questions