skip to Main Content

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:
loading bar

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?

2

Answers


  1. Basically, we can add gif image in image view, and achieve animation.

    1.
    Like this answer
    GIF Image to add

    let jeremyGif = UIImage.gifImageWithName("loader")
    loaderImageView = UIImageView(image: jeremyGif)
    loaderImageView?.contentMode = .left
    loaderImageView?.clipsToBounds = true
    

    As you said, take a timer

    2.
    timerForIncreaseProgress = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(increaseProgress), userInfo: nil, repeats: true)

     @objc func increaseProgress() {
            progress = progress + 0.05
            progressView.progress = progress
            let maxWidth = progressView.frame.width
            let overlayWidth = Float(maxWidth) * progress
            loaderImageView?.frame = CGRect(x: 0, y: 0, width: Double(overlayWidth), height: 13.0)
            print("progressOverlayView.width (progressOverlayView.frame.width)")
            if progress >= 1 {
                timerForIncreaseProgress?.invalidate()
                timerForIncreaseProgress = nil
            }
        }
    

    Hope, it would help you

    Login or Signup to reply.
  2. 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:

    enter image description here

    and we can set the background color of the view with the "between the lines" color:

    enter image description here

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

    enter image description here

    Your original image also appears to have a slight gradient, so we can overlay a translucent gradient for that appearance:

    enter image description here

    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:

    enter image description here

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

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search