Reputation: 612
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 UICollectionViewCell
with 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
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
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