Paulo
Paulo

Reputation: 612

Animating a view inside a collectionviewcell

I am a little stuck with animating a progressView (using this subclass: Linear Progress View).

Just a little bit of background so the code posted below makes sense. I have created a subclass of UICollectionViewCellwith some basic functions. Mainly the reason for this was to reduce boilerplate code. In my collection view I set the cell to my subclass and pass it the model object. All the data is shown correctly, however the progress view doesn't animate. I have tried things like viewWillDisplay on the collection view, but to no avail.

Any suggestions would be greatly appreciated. (Code below)

CollectionViewCell class:

import UIKit
import ChameleonFramework
import Material


class MacrocycleCell: Cell {

var macrocycle:Macrocycle? = nil {
    didSet{
        if let macro = macrocycle, let start = macrocycle?.start, let completion = macrocycle?.completion {

            title.text = macro.title
            let percentage = Date().calculatePercentageComplete(startDate: start, completionDate: completion)
            progress.setProgress(percentage, animated: true)



            let difference = Date.calculateDifferenceInMonthsAndDaysBetween(start: start, end: completion)
            if let monthDiff = difference.month, let dayDiff = difference.day {
                if monthDiff > 0 {
                    detail.text = monthDiff > 1 ? "\(monthDiff) months left" : "\(monthDiff) month left"
                }else{
                    detail.text = dayDiff > 1 ? "\(dayDiff) days left" : "\(dayDiff) day left"
                }
            }


        }

    }
}







let dropView:View = {
    let view = View()
    view.backgroundColor = .white
    view.translatesAutoresizingMaskIntoConstraints = false
    view.depthPreset = DepthPreset.depth2
    return view
}()

let title:UILabel = {
    let label = UILabel()
    label.translatesAutoresizingMaskIntoConstraints = false
    label.textAlignment = .center
    label.font = UIFont.boldSystemFont(ofSize: 18)
    label.textColor = UIColor.flatBlack()
    return label
}()

let progress:LinearProgressView = {
    let progress = LinearProgressView()
    progress.isCornersRounded = true
    progress.barColor = UIColor.flatWhiteColorDark()
    progress.trackColor = UIColor.flatGreen()
    progress.barInset = 0
    progress.minimumValue = 0
    progress.maximumValue = 1
    progress.animationDuration = 1
    progress.translatesAutoresizingMaskIntoConstraints = false
    return progress
}()


let detail:UILabel = {
    let label = UILabel()
    label.translatesAutoresizingMaskIntoConstraints = false
    label.textAlignment = .center
    label.font = UIFont.systemFont(ofSize: 14, weight: UIFont.Weight.light)
    label.textColor = UIColor.flatGray()
    return label
}()


override func drawView() {
    addSubview(dropView)
    dropView.addSubview(title)
    dropView.addSubview(progress)
    dropView.addSubview(detail)

    dropView.topAnchor.constraint(equalTo: topAnchor).isActive = true
    dropView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
    dropView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
    dropView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -2).isActive = true

    progress.centerXAnchor.constraint(equalTo: dropView.centerXAnchor).isActive = true
    progress.centerYAnchor.constraint(equalTo: dropView.centerYAnchor).isActive = true
    progress.heightAnchor.constraint(equalToConstant: 10).isActive = true
    progress.widthAnchor.constraint(equalTo: dropView.widthAnchor, multiplier: 0.8).isActive = true

    title.topAnchor.constraint(equalTo: dropView.topAnchor).isActive = true
    title.leftAnchor.constraint(equalTo: dropView.safeAreaLayoutGuide.leftAnchor).isActive = true
    title.rightAnchor.constraint(equalTo: dropView.safeAreaLayoutGuide.rightAnchor).isActive = true
    title.bottomAnchor.constraint(equalTo: progress.topAnchor, constant: 6).isActive = true

    detail.topAnchor.constraint(equalTo: progress.bottomAnchor, constant: 14).isActive = true
    detail.leftAnchor.constraint(equalTo: dropView.leftAnchor).isActive = true
    detail.rightAnchor.constraint(equalTo: dropView.rightAnchor).isActive = true
    detail.heightAnchor.constraint(equalToConstant: 14).isActive = true
}
}

CollectionViewController Class:

import UIKit

private let reuseIdentifier = "Cell"

class MacrocycleController: UICollectionViewController, UICollectionViewDelegateFlowLayout {

var athlete:Athlete? = nil {
    didSet{
       self.title = athlete?.name

    }
}

override func viewDidLoad() {
    super.viewDidLoad()
    collectionView?.backgroundColor = .flatWhiteColorDark()

    self.collectionView!.register(MacrocycleCell.self, forCellWithReuseIdentifier: reuseIdentifier)


}


override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    // Dispose of any resources that can be recreated.
}




// MARK: UICollectionViewDataSource



func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    return CGSize(width: collectionView.frame.width, height: 90)
}

override func numberOfSections(in collectionView: UICollectionView) -> Int {
    // #warning Incomplete implementation, return the number of sections
    return 1
}


override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    // #warning Incomplete implementation, return the number of items
    return athlete?.macrocycles?.count ?? 0
}

override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! MacrocycleCell
    cell.macrocycle = athlete?.macrocycles?[indexPath.item]



    return cell
}


}

I am sure it is something simple, that I have forgotten to implement.

Thanks, you beautiful people!

Upvotes: 0

Views: 1298

Answers (2)

ozzieozumo
ozzieozumo

Reputation: 436

Here is another answer, which keeps all of the logic in the cell class and doesn't use the WillDisplayCell delegate method of the collection controller.

var progress: Float = 0.0
@IBOutlet var progressBar: LinearProgressView!
var drawnAtLeastOnce = false

func animateIfNeeded() {
    if progressBar.progress != self.progress {
        progressBar.setProgress(progress, animated: true)
    }
}
override func layoutSubviews() {
    super.layoutSubviews()
    if drawnAtLeastOnce {
        animateIfNeeded()
    }
}

override func draw(_ rect: CGRect) {
    super.draw(rect)
    if !drawnAtLeastOnce {
        drawnAtLeastOnce = true
        setNeedsLayout()
    }
}

The key to this answer is detecting the end of the first drawing pass and suppressing animation until that time.

This works pretty well for the first screenful of cells. Cells that scroll into view may be pre-drawn, so you won't see animation on those cells. (at least that's what I observed when testing with a tableview).

Upvotes: 0

ozzieozumo
ozzieozumo

Reputation: 436

You are seeing no animation because there is no visible change to be animated. Specifically, you are calling setProgress from didSet, before the cell is displayed, and then you never call it again.

To see an animation, you would have to allow the view to display with some initial value (0%) and then call setProgress again, animating the change to the true value.

What you are trying to do is not a good idea from a UI standpoint -- in principle, a progress bar should move only when some actual meaningful progress occurs (user action, download etc). Therefore, my answer to your question is to to use the progress bar without animation in this case.

That being said, there are legitimate cases where you might want to update a progress bar in a cell without having to reload the cell. For example, if you had some background tasks running that were periodically reporting progress relevant to each cell. There are copious answers demonstrating different ways to do that kind of update.

Also, there a various hacks that have been suggested for triggering updates to occur immediately after a cell has been displayed. Search questions for the non-existent "DidDisplayCell" delegate method if you want to see some of those ideas. You could definitely make animation happen after cell display, but I want to steer you away from the temptation to use animation for artificial purposes.

Here's some code fragments to illustrate how you could do this using asyncAfter from WillDisplay (I tested this with tableview):

// This would be in your cell class
var progress: Float = 0.0
@IBOutlet var progressBar: LinearProgressView!

func animateIfNeeded() {

    if progressBar.progress != self.progress {
        progressBar.setProgress(progress, animated: true)
    }
}

// This would be in your delegate (I tested with tableview)
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {

    DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
        let animateCell = cell as! Cell
        animateCell.animateIfNeeded()
    }
}

And obviously, in the didSet you would just save the calculated value into an instance variable (progress: Float) and call progressBar.setProgress(0,animated: false).

Upvotes: 2

Related Questions