0

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!

MQLN
  • 2,292
  • 2
  • 18
  • 33
Paulo
  • 602
  • 1
  • 8
  • 20
  • So `macrocycle` is set upon `viewDidLoad` and upon `cellForItem`, and it's not animating at either occurance? – MQLN Nov 11 '17 at 21:35
  • The [macrocycle] is set from the previous vc and pushed, using a didSet call. The cell instance is the same, but within the collectionViewCell macrocycle didSet. Does that make sense? – Paulo Nov 11 '17 at 21:38
  • I understand, just trying to figure out where you actual animation is getting called. Is the intent of the question to figure out where to put a call to animate the cell upon it's deque? – MQLN Nov 11 '17 at 21:40
  • Kinda. The LinearProgressView has it's own animate function called setProgress(value: Float, animated:Bool). I am setting this when I set the macrocycle variable within the cell object. I have tried setting a stupidly high value in the setProgress method to see if the animation is occurring before the view is visible, but still nothing. It's probably me, being a muppet! – Paulo Nov 11 '17 at 21:43
  • I can post some more code/images, if you think that would help? – Paulo Nov 11 '17 at 22:12
  • @MacLean After re-reading your comment, I believe you are correct. I’m not sure where to call the set progress from within the cell. I did try putting it in the cell’s unit function, but this didn’t seem to work either. – Paulo Nov 11 '17 at 23:21

2 Answers2

2

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).

ozzieozumo
  • 426
  • 2
  • 8
  • So, would you suggest that I put the value to zero in the Init, then animate? – Paulo Nov 12 '17 at 07:27
  • You would set the progress bar to zero in didSet, and save the calculated value. That is the easy part. The harder part is figuring out when/how to call setProgress a second time after the cell has been displayed. Since there is no delegate method "DidDisplayCell", you have to come up with your own technique. Hints (three different options): cell.layoutSubviews, cell.didMoveToWindow, or dispatch asyncAfter called from WillDisplayCell. – ozzieozumo Nov 12 '17 at 17:19
  • Okay, I will have a look at this. :) I will mark it as correct, if the suggestions pan out – Paulo Nov 12 '17 at 17:43
  • No worries Paulo. I've updated the answer with some code fragments illustrating one of the techniques that I hinted at (asyncAfter from WillDisplayCell). I tested it with a basic table view. – ozzieozumo Nov 12 '17 at 19:03
  • So would you suggest I create a protocol to send back to the controller from the cell, in order to animate? – Paulo Nov 12 '17 at 21:18
  • Right, I ended up doing something similar to your suggested solution. I basically created a variable that the cell set its progress to, if this was greater than 0, I performed the animation within the controller class. Whilst this is a bit messy, it did create the visual appearance I was looking for. Thanks! I will accept this as right, but for any future viewers, it might be worth updating the answer, as the point of the question was a view inside of the collectionviewcell and not in the controller view. :) Thank you, once again! – Paulo Nov 12 '17 at 21:26
  • @Paulo - I just posted another answer that shows how you might achieve the same effect using only the cell, without overriding WillDisplayCell in the controller. I'm not sure that I like this any better but maybe somebody will find it useful. – ozzieozumo Nov 13 '17 at 18:12
0

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).

ozzieozumo
  • 426
  • 2
  • 8
  • This looks like a better approach. When I get back to my computer, I’ll see if this works. Thanks – Paulo Nov 13 '17 at 18:10