I'm trying to create a progress bar animation in Swift. I designed a patterned image to simulate progress but can't directly apply this animation to a UIProgressView. Instead, I placed a UIView on top to mimic this behavior.
I made a symmetric pic: green background, with dark green horizontal lines.
As I understood, since I cannot attach it on UIProgressBar, I added a UIView to mimic ProgressBar.
The idea is to have something like this:
Here's my current setup:
import UIKit
class SideMenuButtons: UIViewController {
var progressOverlayView: UIView!
@IBOutlet weak var energyReloadingIndicator: UIProgressView!
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
}
private func setupUI() {
progressOverlayView = UIView()
if let patternImage = UIImage(named: "striped-pattern-repeated") {
progressOverlayView.backgroundColor = UIColor(patternImage: patternImage)
} else {
progressOverlayView.backgroundColor = .red
}
energyReloadingIndicator.addSubview(progressOverlayView)
updateProgressOverlayViewWidth()
}
private func updateProgressOverlayViewWidth() {
let progress = CGFloat(energyReloadingIndicator.progress)
let maxWidth = energyReloadingIndicator.frame.width
let overlayWidth = maxWidth * progress
let overlayHeight = energyReloadingIndicator.frame.height
progressOverlayView.frame = CGRect(x: 0, y: 0, width: overlayWidth, height: overlayHeight)
}
}
How can I animate the pattern in progressOverlayView to achieve a continuous moving effect?
Let's look at one approach that does not require an animated gif, or a pattern image..
We can use a UIBezierPath
with a CAShapeLayer
to create the pattern in a custom view subclass:
and we can set the background color of the view with the "between the lines" color:
Now we can use CABasicAnimation(keyPath: "position.x")
to "slide" that layer to the left. We create the lines path to be wider than the view so we don't see a gap on the right (ignore the "hiccup" in this animation, it's only there because it's a partial capture):
Your original image also appears to have a slight gradient, so we can overlay a translucent gradient for that appearance:
Now we can create a custom "progress" view by embedding that animated shape view in a "container" view, and adjust the width of the container to reflect the progress:
Here is some sample code...
AnimatedPatternView: all of the size, color, speed, etc properties are at the top to make it easy to adjust them to your liking:
class AnimatedPatternView: UIView {
private let angledLinesShapeLayer = CAShapeLayer()
private let overlayGradLayer = CAGradientLayer()
// let's put all of our appearance variables here
// in one place, to make it easier to adjust them
// this will be the amount of time (in seconds) that the
// animation takes to slide "one tile" to the left
private let animSpeed: Double = 0.5
// "tile" width
private let tileWidth: CGFloat = 40.0
// we want an angled line, not the full width
private let angledLineBottomOffset: CGFloat = 32.0
// thickness of the lines
private let angledLineThickness: CGFloat = 14.0
// the angled-line and "space between" colors
private let angledLineColor: UIColor = UIColor(red: 0.557, green: 0.420, blue: 0.863, alpha: 1.0)
private let spaceBetweenColor: UIColor = UIColor(red: 0.624, green: 0.494, blue: 0.886, alpha: 1.0)
// soft overlay gradient
private let overlayGradientColors: [CGColor] = [
UIColor.black.withAlphaComponent(0.2).cgColor,
UIColor.white.withAlphaComponent(0.1).cgColor,
UIColor.black.withAlphaComponent(0.05).cgColor,
]
// gradient angle
private let gradStartPoint: CGPoint = .init(x: 0.250, y: 0.0)
private let gradEndPoint: CGPoint = .init(x: 0.750, y: 1.0)
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
self.backgroundColor = spaceBetweenColor
angledLinesShapeLayer.strokeColor = angledLineColor.cgColor
angledLinesShapeLayer.lineWidth = angledLineThickness
angledLinesShapeLayer.lineCap = .square
overlayGradLayer.colors = overlayGradientColors
overlayGradLayer.startPoint = gradStartPoint
overlayGradLayer.endPoint = gradEndPoint
self.layer.addSublayer(angledLinesShapeLayer)
self.layer.addSublayer(overlayGradLayer)
self.layer.masksToBounds = true
self.clipsToBounds = true
}
override func layoutSubviews() {
super.layoutSubviews()
let bez = UIBezierPath()
var x: CGFloat = 0.0
while x < bounds.width + tileWidth {
bez.move(to: .init(x: x, y: bounds.minY))
bez.addLine(to: .init(x: x + angledLineBottomOffset, y: bounds.maxY))
x += tileWidth
}
angledLinesShapeLayer.path = bez.cgPath
// gradient layer needs to match the view bounds
overlayGradLayer.frame = bounds
angledLinesShapeLayer.removeAllAnimations()
let animation = CABasicAnimation(keyPath: "position.x")
animation.fromValue = 0.0
animation.toValue = -tileWidth
animation.duration = animSpeed
animation.repeatCount = .infinity
angledLinesShapeLayer.add(animation, forKey: "stripeAnim")
}
}
EnergyProgressView: view subclass that uses the AnimatedPatternView
and a container view to simulate a progress bar:
class EnergyProgressView: UIView {
public var progress: Float {
set { _progress = newValue }
get { return _progress }
}
private var _progress: Float = 1.0 {
didSet {
wConstraint.isActive = false
wConstraint = containerView.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: CGFloat(_progress))
wConstraint.isActive = true
}
}
private let animPatternView = AnimatedPatternView()
private let containerView = UIView()
// this will control the width of the container
// using progress as a percentage of the width
private var wConstraint: NSLayoutConstraint!
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
animPatternView.translatesAutoresizingMaskIntoConstraints = false
containerView.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(animPatternView)
self.addSubview(containerView)
containerView.clipsToBounds = true
// we'll be modifying the width of containerView to reflect the progress
wConstraint = containerView.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 1.0)
NSLayoutConstraint.activate([
containerView.topAnchor.constraint(equalTo: self.topAnchor, constant: 0.0),
containerView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 0.0),
containerView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: 0.0),
wConstraint,
// even though animPatternView is a subview of containerView
// we constrain animPatternView to self so it doesn't resize
// when the progress changes
animPatternView.topAnchor.constraint(equalTo: self.topAnchor, constant: 0.0),
animPatternView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 0.0),
animPatternView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: 0.0),
animPatternView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: 0.0),
])
self.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
self.layer.masksToBounds = true
self.layer.cornerRadius = 8.0
}
func setProgress(_ p: Float, animated: Bool = false) {
_progress = p
if animated {
UIView.animate(withDuration: 0.3, animations: {
self.layoutIfNeeded()
})
}
}
}
ViewController: example view controller with the custom progress bar and a slider to update the progress:
class ViewController: UIViewController {
let myProgressView = EnergyProgressView()
// let's add a label and a slider so we can dynamically set the progress
let pctLabel = UILabel()
let slider = UISlider()
override func viewDidLoad() {
super.viewDidLoad()
myProgressView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(myProgressView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
myProgressView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
myProgressView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
myProgressView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
myProgressView.heightAnchor.constraint(equalToConstant: 32.0),
])
pctLabel.textAlignment = .center
pctLabel.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(pctLabel)
slider.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(slider)
NSLayoutConstraint.activate([
pctLabel.topAnchor.constraint(equalTo: myProgressView.bottomAnchor, constant: 40.0),
pctLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
pctLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
slider.topAnchor.constraint(equalTo: pctLabel.bottomAnchor, constant: 40.0),
slider.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
slider.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
])
slider.addTarget(self, action: #selector(handleSlider(_:)), for: .valueChanged)
slider.value = 0.5
myProgressView.progress = 0.5
updatePctLabel()
}
@objc func handleSlider(_ sender: UISlider) {
myProgressView.progress = sender.value
updatePctLabel()
}
func updatePctLabel() {
let v = myProgressView.progress
pctLabel.text = String(format: "%0.2f %%", v * 100.0)
}
}
Please Note: this is Example Code Only!!! It should be used as a learning tool, and should not be considered "Production Ready."