I'm currently trying to add an animation to the user's xp bar.
If I had the animation in the collectionViewController, the animation is well loaded (once). However, if I had the animation within the headerView (because I would like to add the bar on the profile picture) the bar is launched more than one time:

This is my code (headerViewCell):
let shapeLayerXp = CAShapeLayer()
override func layoutSubviews() {
super.layoutSubviews()
showUserXp()
self.animateXp(toValue: 1)
}
func showUserXp() {
let center = profileImage.center
let circularPath = UIBezierPath(arcCenter: center, radius: 40, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)
shapeLayerXp.path = circularPath.cgPath
let color = UIColor(red: 122 / 255, green: 205 / 255, blue: 186 / 255, alpha: 1)
shapeLayerXp.strokeColor = color.cgColor
shapeLayerXp.lineWidth = 4
shapeLayerXp.fillColor = UIColor.clear.cgColor
shapeLayerXp.lineCap = kCALineCapRound
shapeLayerXp.strokeEnd = 0
self.contentView.layer.addSublayer(shapeLayerXp)
}
func animateXp(toValue: Int) {
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
basicAnimation.toValue = toValue
basicAnimation.duration = 2
basicAnimation.fillMode = kCAFillModeForwards
basicAnimation.isRemovedOnCompletion = false
shapeLayerXp.add(basicAnimation, forKey: "urSoBasic")
}
The headerViewCell is launched like that:
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let headerViewCell = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionHeader, withReuseIdentifier: "Header", for: indexPath) as! UserHeaderView
// ....
return cell
}
Right assuming there is only one header cell ever in the controller unless the data is changed (e.g. a different user) then you can set a property on the view controller to indicate if the animation has been shown.
Something like this would do:
var animatedHeader = false // Starts false because we want an animation the first time.
Then when obtaining the header cell for the first time you can decide to fire the animation or not like this:
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let headerViewCell = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionHeader, withReuseIdentifier: "Header", for: indexPath) as! UserHeaderView
if !self.animatedHeader {
cell.showUserXp()
cell.animateXp()
self.animatedHeader = true
}
return cell
}
The showUserXp() and animateXp() methods will need to be public of course.
Using this method the header cell will only animate the first time it is dequeued and therefore displayed.
If you did want to animate it again you just need to reset the animateHeader property and reload the collection view (or just the header).
If there is more than one header then you will need to keep track of each of them separately.
Edit: This does require (accidentally) the same cell to be used because of how the showXP and animateXP functions are defined. If I was doing this myself I would probably use something more like this approach:
func showUserXp(animated: Bool) {
let center = profileImage.center
let circularPath = UIBezierPath(arcCenter: center, radius: 40, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)
shapeLayerXp.path = circularPath.cgPath
let color = UIColor(red: 122 / 255, green: 205 / 255, blue: 186 / 255, alpha: 1)
shapeLayerXp.strokeColor = color.cgColor
shapeLayerXp.lineWidth = 4
shapeLayerXp.fillColor = UIColor.clear.cgColor
shapeLayerXp.lineCap = kCALineCapRound
shapeLayerXp.strokeEnd = 0
self.contentView.layer.addSublayer(shapeLayerXp)
if animated {
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
basicAnimation.toValue = toValue
basicAnimation.duration = 2
basicAnimation.fillMode = kCAFillModeForwards
basicAnimation.isRemovedOnCompletion = false
shapeLayerXp.add(basicAnimation, forKey: "urSoBasic")
} else {
shapeLayerXp.strokeEnd = 1
}
}
Then you would use it like this:
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let headerViewCell = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionHeader, withReuseIdentifier: "Header", for: indexPath) as! UserHeaderView
if self.animatedHeader {
cell.showUserXp(animated: false)
} else {
cell.showUserXp(animated: true)
self.animatedHeader = true
}
return cell
}
So now you can show the header cell using an animation or not and whether you do use the animation is controlled by the animatedHeader property. This now no longer relies on a specific cell being dequeued.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With