Do you need help on a specific subject? Use the contact form (Request a blog entry) on the right hand side.

2016-12-05

A general purpose progress bar with label | Swift Code Library

Its been a while as I have been tackling one "urgent" thing after another. No time for blog updates. While I am not assuming that this will change soon, I do have a worthwhile addition to the Swift Code library.

In my current iOS project I needed a progress bar with an associated label. This tracks the rate at which messages are being transferred/received over the internet. Since there is a clear label associated with each message this label should be displayed along with the update of the progress bar. The amount of messages is variable and can even be just one. To give the user useful feedback it was decided that each message should be visible for a small amount of time. Enough to recognize the message, but no more than necessary.

The following class was created:

    final class ProgressBarWithLabel {
        
        struct Info {
            let progress: Float?
            let label: String?
        }
        
        var info: Array<Info> = []
        let timeBetweenUpdates: DispatchTimeInterval
        var timeOfPreviousUpdate: DispatchTime = DispatchTime.now()
        let progressBar: UIProgressView
        let progressLabel: UILabel?
        let mainQueue = DispatchQueue.main // Must be main because the GUI is updated from here
        
        init(progressBar: UIProgressView, progressLabel: UILabel?, timeBetweenUpdates: DispatchTimeInterval) {
            self.timeBetweenUpdates = timeBetweenUpdates
            self.progressBar = progressBar
            self.progressLabel = progressLabel
            self.progressBar.progress = 0.0
            self.progressLabel?.text = ""
        }
        
        func add(info: Info) {
            
            mainQueue.async {
                
                [weak self] in
                
                // Add the new info as the last to be displayed
                self?.info.insert(info, at: 0)
                
                // Make sure there will be an update of the gui
                self?.update()
            }
        }
        
        private func update() {
            
            // Check if a new update can be made
            guard DispatchTime.now() >= timeOfPreviousUpdate + timeBetweenUpdates else {
                

                if info.count > 1 { return }

                // Wait until the necessary delay has expired then try again
                mainQueue.asyncAfter(
                    deadline: timeOfPreviousUpdate + timeBetweenUpdates,
                    execute: {
                        [weak self] () -> () in
                        self?.update()
                    }
                )
                return
            }
            
            // An update will be made, set the time of "previous" to current time
            // This could also be done after the factual update, but doing it before the info test can prevent "gui glitches" if somebody modifies the code after the test. (Defensive Programming)
            timeOfPreviousUpdate = DispatchTime.now().uptimeNanoseconds
                
            // Fetch info to be displayed
            guard let forDisplay = info.popLast() else { return }
            
            // Update GUI, reset the GUI if there is no new data
            progressBar.progress = forDisplay.progress ?? 0.0
            progressLabel?.text = forDisplay.label ?? ""
            
            // If there is more info to be displayed, wait a little
            if info.count > 0 {
                mainQueue.asyncAfter(
                    deadline: timeOfPreviousUpdate + timeBetweenUpdates,
                    execute: {
                        [weak self] () -> () in
                        self?.update()
                    }
                )
            }
        }

    }

Note that this class does not contain the GUI elements itself, they must be specified when an instance is created. Since these GUI elements are assigned by the runtime, special care should be taken to ensure that they are not deallocated when the view goes out of sight and a possible low-memory condition occurs.

I used the following code that ensures the cell stays allocated as long as the table is in use:

var progressCell: FetchAutoQuotesCell?

.... and then in tableView_cellForRowAt:


let cell = progressCell ?? tableView.dequeueReusableCell(withIdentifier: "ProgressCell") as? ProgressCell

if progressCell == nil {
    progressCell = cell
    progressBarWithLabel = ProgressBarWithLabel(progressBar: (cell?.progressBar)!, progressLabel: (cell?.progressLabel)!, timeBetweenUpdates: DispatchTimeInterval.milliseconds(250))
}

This may be overkill, but it won't hurt. (The assignment of the GUI elements themselves is probably enough to keep the cell from being deallocated. OTOH, it might not be enough to ensure that the same cell is reused again and again under all conditions)

Usage of the ProgressBarWithLabel class is relatively easy. The initialization was already shown, the only other call is the add function.

It can be called as follows:

progressBarWithLabel?.add(info: ProgressBarWithLabel.Info(progress: (Float(count) / Float(totalCount)), label: message))

To ensure that the progress indicators are reset, call the add function with an empty Info struct.


Happy coding...

Did this help?, then please help out a small independent.
If you decide that you want to make a small donation, you can do so by clicking this
link: a cup of coffee ($2) or use the popup on the right hand side for different amounts.
Payments will be processed by PayPal, receiver will be sales at balancingrock dot nl
Bitcoins will be gladly accepted at: 1GacSREBxPy1yskLMc9de2nofNv2SNdwqH

We don't get the world we wish for... we get the world we pay for.

No comments:

Post a Comment