Add Progress bar to UIAlertController with showing

2019-09-13 05:09发布

问题:

I try to add progress bar on my application. I found the question on How to add Progress bar to UIAlertController? but it didn't show how to update the progress bar. I simplify the code as below but it didn't update the progress bar (it shows only once the progress is completed). What did i over look? Thank you for your help.

override func viewDidLoad() {
    super.viewDidLoad()
    var topViewController = UIApplication.shared.delegate!.window!!.rootViewController!
    while (topViewController.presentedViewController != nil){
        topViewController = topViewController.presentedViewController!
    }
    DispatchQueue.main.async(execute: {
        let alert = UIAlertController(title: "downloading", message: "pls wait", preferredStyle: .alert)
        let progressBar = UIProgressView(progressViewStyle: .default)
        progressBar.setProgress(0.0, animated: true)
        progressBar.frame = CGRect(x: 10, y: 70, width: 250, height: 0)
        alert.view.addSubview(progressBar)
        topViewController.present(alert, animated: true, completion: nil)
        var progress: Float = 0.0
        repeat {
            DispatchQueue.global(qos: .background).async(execute: {
                progress += 0.01
                print (progress)
                DispatchQueue.main.async(flags: .barrier, execute: {
                    progressBar.setProgress(progress, animated: true)
                })
            })
        } while progress < 1.0
    })
}

回答1:

One could get just a little confused by all those dispatch queues in your code :-)

I try to explain the problem:

The first (outmost) DispatchQueue.main.async is excecuted in the main (UI) thread. It creates the UIAlertController, stuffs the UIProgressView into it and displays the dialog. Then it performs in the main thread some time-critial job (repeat-while-loop). Never do so, because it blocks the main thread. Since the main thread is blocked, no updates are reflected in the UI controls, so you don't see progress changes.

Therefore,

  • I put the time critical work item (repeat loop) into the global queue (not the main queue) as an asynchronous work item
    • Within that work item, I'll dispatch the progress bar updates into the main queue (to be executed in the main thread)
    • At the end, when everything is done, I dismiss the UIAlertController

You only need the first Dispatch when your alert view is shown from a method like viewDidLoad or viewWillAppear, e.g. from a point in time when the view isn't displayed yet. If you call it from a button callback, or viewDidAppear (e.g. view is visible), you could just skip that outer Dispatch.

Here you go - I also put in some sleep time and modified the GCD-Calls a litte to use trailing closures:

override func viewDidLoad() {
    super.viewDidLoad()
    var topViewController = UIApplication.shared.delegate!.window!!.rootViewController!
    while (topViewController.presentedViewController != nil){
        topViewController = topViewController.presentedViewController!
    }

    // Usage of GCD here is only necessary because it's called from
    // viewDidLoad. If called by viewDidAppear or some action callback, you 
    // don't need it:
    DispatchQueue.main.async {
        let alert = UIAlertController(title: "downloading", message: "pls wait", preferredStyle: .alert)
        let progressBar = UIProgressView(progressViewStyle: .default)
        progressBar.setProgress(0.0, animated: true)
        progressBar.frame = CGRect(x: 10, y: 70, width: 250, height: 0)
        alert.view.addSubview(progressBar)
        topViewController.present(alert, animated: true, completion: nil)
        var progress: Float = 0.0
        // Do the time critical stuff asynchronously
        DispatchQueue.global(qos: .background).async {
            repeat {
                progress += 0.1
                Thread.sleep(forTimeInterval: 0.25)
                print (progress)
                DispatchQueue.main.async(flags: .barrier) {
                    progressBar.setProgress(progress, animated: true)
                }
            } while progress < 1.0
            DispatchQueue.main.async {
                alert.dismiss(animated: true, completion: nil);
            }
        }
    }
}


回答2:

After help from Andreas comment. I work out with this code.

 override func viewDidLoad() {
    super.viewDidLoad()
    var topViewController = UIApplication.shared.delegate!.window!!.rootViewController!
    while (topViewController.presentedViewController != nil){
        topViewController = topViewController.presentedViewController!
    }
    DispatchQueue.main.async(execute: {
        let alert = UIAlertController(title: "loading", message: "wait please", preferredStyle: .alert)

        let rect = CGRect(x: 10, y: 70, width: 250, height: 0)
        let progressView = UIProgressView(frame: rect)
        progressView.progress = 0.0
        progressView.tintColor = UIColor.blue
        alert.view.addSubview(progressView)
        topViewController.present(alert, animated: true, completion: nil)
        var progress = 0.0
        DispatchQueue.global(qos: .background).async(execute: {
            repeat {
                progress += 0.0001
                print (progress)
                DispatchQueue.main.async(flags: .barrier, execute: {
                    progressView.progress = Float(progress)
                })
            } while progress < 1.0
            DispatchQueue.main.async(execute: {
                alert.dismiss(animated: true, completion: nil);
            })
        })

    })
}