Mert Diricanlı
Mert Diricanlı

Reputation: 85

Reset UIProgressView and begin animating immediately with Swift 3

I have a progress view like the one Snapchat and Instagram Stories have. My content changes after the progress view reaches to the end or when tapped on a button.

I'm reseting the progress view when content changes. Everything works as expected while there is no intervention. But when tapped on next button the progress view doesn't start again until the other loop executes.

You can see the video here quickly.

I came across this question while I was researching, I have the same scenario but I couldn't apply the principle as a newbie with swift 3.

Here is my code, any help would be highly appreciated:

func startTimer(){
    self.timer = Timer.scheduledTimer(timeInterval: 3, target: self, selector: #selector(myVievController.nextItem), userInfo: nil, repeats: false)
}

func updateProgressBar() {
    self.progressView.setProgress(1.0, animated: false)
    UIView.animate(withDuration: 2.8, animations: {() -> Void in
        self.progressView.layoutIfNeeded()
    }, completion: { finished in
        if finished {
            self.progressView.setProgress(0, animated: false)
        }
    })
}

func nextItem(){
    // ...
    self.updateUI(item: self.myCurrentItem)
    // ...
}

func updateUI(item:MyItem){
    self.updateProgressBar()
    self.timer.invalidate()
    self.startTimer()

    // Clear old item and fill new values etc...
}

@IBAction func nextButtonPressed(_ sender: UIButton) {
    self.nextItem()
}

Upvotes: 2

Views: 5672

Answers (3)

Michael N
Michael N

Reputation: 559

Example

enter image description here

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var progressView: UIProgressView!

    var timer : Timer?
    var timerCount = 0
    var imageArray : Array<UIImage> = []

    // MARK: - Lifecycle -

    override func viewDidLoad() {

        super.viewDidLoad()

        // create gradient images

        buildImageArray(duration: 3, highlightPercentage: 10.0, mainColor: .green, highlightColor: .yellow, imageWidth: Double(progressView.frame.width))

        // set progressView to first image

        progressView.progressImage = imageArray[0]

        // set progressView progress

        progressView.progress = 0.7

        // schedule timer

        if self.timer == nil {

            self.timer = Timer.scheduledTimer(timeInterval: 1/60, target: self, selector: #selector(updateGradient), userInfo: nil, repeats: true)
            RunLoop.main.add(self.timer!, forMode: .common)

        }

    }

    func buildImageArray(duration: Int, highlightPercentage: Double, mainColor: UIColor, highlightColor: UIColor, imageWidth: Double){

        let keyFrames = duration * 60

        for i in 0..<keyFrames {

            // Drawing code

            let frame = CGRect(x: 0.0, y: 0.0, width: imageWidth, height: 1.0)
            let layer = CAGradientLayer()
            layer.frame = frame
            layer.startPoint = CGPoint(x: 0.0, y: 0.5)
            layer.endPoint = CGPoint(x: 1.0, y: 0.5)

            var colors : [UIColor] = []

            for n in 0..<keyFrames {

                colors.append(mainColor)

            }

            let highlightKeyFrames : Int = Int(floor(Double(keyFrames) * (highlightPercentage/100)))

            // 300 * .3 = 90 kf

            for x in 0..<highlightKeyFrames {

                let p = i+x

                if p < keyFrames {

                    colors[p] = highlightColor

                }

            }

            layer.colors = colors.map { $0.cgColor }

            layer.bounds = frame

            let image = UIImage.imageWithLayer(layer: layer)

            imageArray.append(image)

        }

    }

    // updateGradient

    @objc func updateGradient(){

        // crop image to match progress

        let newWidth = self.progressView.frame.width * CGFloat(progressView.progress)

        let progressImage = imageArray[timerCount].crop(rect: CGRect(x: 0, y: 0, width: newWidth, height: 1))

        progressView.progressImage = progressImage

        // increment timer

        timerCount = timerCount + 1

        if timerCount >= imageArray.count {

            timerCount = 0

        }

    }

}

extension UIImage {

    class func imageWithLayer(layer: CALayer) -> UIImage {
        UIGraphicsBeginImageContextWithOptions(layer.bounds.size, layer.isOpaque, 0.0)
        layer.render(in: UIGraphicsGetCurrentContext()!)
        let img = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return img!
    }

    func crop( rect: CGRect) -> UIImage {
        var rect = rect
        rect.origin.x*=self.scale
        rect.origin.y*=self.scale
        rect.size.width*=self.scale
        rect.size.height*=self.scale

        let imageRef = self.cgImage!.cropping(to: rect)
        let image = UIImage(cgImage: imageRef!, scale: self.scale, orientation: self.imageOrientation)
        return image
    }

}

Upvotes: 0

Mikk R&#228;tsep
Mikk R&#228;tsep

Reputation: 512

Could also do it with a subclass:

class HelloWorld: UIProgressView {

    func startProgressing(duration: TimeInterval, resetProgress: Bool, completion: @escaping (Void) -> Void) {
        stopProgressing()

        // Reset to 0
        progress = 0.0
        layoutIfNeeded()

        // Set the 'destination' progress
        progress = 1.0

        // Animate the progress
        UIView.animate(withDuration: duration, animations: {
            self.layoutIfNeeded()

        }) { finished in
            // Remove this guard-block, if you want the completion to be called all the time - even when the progression was interrupted
            guard finished else { return }

            if resetProgress { self.progress = 0.0 }

            completion()
        }
    }

    func stopProgressing() {
        // Because the 'track' layer has animations on it, we'll try to remove them
        layer.sublayers?.forEach { $0.removeAllAnimations() }
    }
}

This can be used by calling -startProgressing() when ever you need to start the progressView again from the start. The completion is called when it has finished the animation, so you can change the views etc.

An example of use:

progressView.startProgressing(duration: 5.0, resetProgress: true) {
    // Set the next things you need... change to a next item etc.
}

Upvotes: 1

Ramon Vasconcelos
Ramon Vasconcelos

Reputation: 1466

You probably need something like this to update your progress bar: self.progressView.setProgress(0, animated: false)

func updateProgressBar() {
    DispatchQueue.main.async {
        self.progressView.setProgress(0.1, animated: false)
    }
    if self.progressView.Progress == 1.0 {
       self.timer.invalidate()
    } else {
       self.timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(myVievController.updateProgressBar), userInfo: nil, repeats: false)
    }
}

Then you can invalidate the timer and stop the update when you are at 100%

Upvotes: 0

Related Questions