dslkfjsdlfjl dslkfjsdlfjl - 1 month ago 12
Swift Question

Updating the UI Using Dispatch_Async in Swift

In my code I have a simple for loop that loops 100 times with nested for loops to create a delay. After the delay, I am updating a progress view element in the UI through a dispatch_async. However, I cannot get the UI to update. Does anyone know why the UI is not updating? Note: The print statement below is used to verify that the for loop is looping correctly.

for i in 0..<100 {

//Used to create a delay
for var x = 0; x<100000; x++ {
for var z = 0; z<1000; z++ {

}
}

println(i)

dispatch_async(dispatch_get_main_queue()) {
// update some UI
self.progressView.setProgress(Float(i), animated: true)

}
}

Rob Rob
Answer

Three observations, two basic, one a little more advanced:

  1. Your loop will not be able to update the UI in that main thread unless the loop itself is running on another thread. So, you can dispatch it to some background queue. In Swift 3:

    DispatchQueue.global(qos: .utility).async {
        for i in 0 ..< kNumberOfIterations {
    
            // do something time consuming here
    
            DispatchQueue.main.async {
                // now update UI on main thread
                self.progressView.setProgress(Float(i) / Float(kNumberOfIterations), animated: true)
            }
        }
    }
    

    In Swift 2:

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
        for i in 0 ..< kNumberOfIterations {
    
            // do something time consuming here
    
            dispatch_async(dispatch_get_main_queue()) {
                // now update UI on main thread
                self.progressView.setProgress(Float(i) / Float(kNumberOfIterations), animated: true)
            }
        }
    }
    
  2. Also note that the progress is a number from 0.0 to 1.0, so you presumably want to divide by the maximum number of iterations for the loop.

  3. If UI updates come more quickly from the background thread than the UI can handle them, the main thread can get backlogged with update requests (making it look much slower than it really is). To address this, one might consider using dispatch source to decouple the "update UI" task from the actual background updating process.

    One can use a DispatchSourceUserDataAdd (in Swift 2, it's a dispatch_source_t of DISPATCH_SOURCE_TYPE_DATA_ADD), post add calls (dispatch_source_merge_data in Swift 2) from the background thread as frequently as desired, and the UI will process them as quickly as it can, but will coalesce them together when it calls data (dispatch_source_get_data in Swift 2) if the background updates come in more quickly than the UI can otherwise process them. This achieves maximum background performance with optimal UI updates, but more importantly, this ensures the UI won't become a bottleneck.

    So, first declare some variable to keep track of the progress:

    var progressCounter: UInt = 0
    

    And now your loop can create a source, define what to do when the source is updated, and then launch the asynchronous loop which updates the source. In Swift 3 that is:

    progressCounter = 0
    
    // create dispatch source that will handle events on main queue
    
    let source = DispatchSource.makeUserDataAddSource(queue: .main)
    
    // tell it what to do when source events take place
    
    source.setEventHandler() { [unowned self] in
        self.progressCounter += source.data
    
        self.progressView.setProgress(Float(self.progressCounter) / Float(kNumberOfIterations), animated: true)
    }
    
    // start the source
    
    source.resume()
    
    // now start loop in the background
    
    DispatchQueue.global(qos: .utility).async {
        for i in 0 ..< kNumberOfIterations {
            // do something time consuming here
    
            // now update the dispatch source
    
            source.add(data: 1)
        }
    }
    

    In Swift 2:

    progressCounter = 0
    
    // create dispatch source that will handle events on main queue
    
    let source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue());
    
    // tell it what to do when source events take place
    
    dispatch_source_set_event_handler(source) { [unowned self] in
        self.progressCounter += dispatch_source_get_data(source)
    
        self.progressView.setProgress(Float(self.progressCounter) / Float(kNumberOfIterations), animated: true)
    }
    
    // start the source
    
    dispatch_resume(source)
    
    // now start loop in the background
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
        for i in 0 ..< kNumberOfIterations {
    
            // do something time consuming here
    
            // now update the dispatch source
    
            dispatch_source_merge_data(source, 1);
        }
    }