Updating the Ui Using Dispatch_Async in Swift

Updating the UI Using Dispatch_Async in Swift

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);
    }
    }

Updating UI Dispatch_Async background download Swift

As I didn't receive / find any solution to my problem I went back to an higher level and changed the way to communicate between my classes to handle ui changes based on background download thread progression.

Instead of using protocols, I went for Notifications and it solved my problem.

Inside the download class:

func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {

println("downloaded \(100*totalBytesWritten/totalBytesExpectedToWrite)")

//NOTIFICATION
// notify download progress!
var fileInfo = [NSObject:AnyObject]()
fileInfo["fileId"] = fileDownloader.storageInfo[downloadTask.taskIdentifier]!["id"] as! Int!
fileInfo["fileCurrent"] = Float(totalBytesWritten)
fileInfo["fileTotal"] = Float(totalBytesExpectedToWrite)

let defaultCenter = NSNotificationCenter.defaultCenter()
defaultCenter.postNotificationName("DownloadProgressNotification",
object: nil,
userInfo: fileInfo)

}

inside the view controller:

override func viewDidLoad() {
super.viewDidLoad()

// ready for receiving notification
let defaultCenter = NSNotificationCenter.defaultCenter()
defaultCenter.addObserver(self,
selector: "handleCompleteDownload:",
name: "DownloadProgressNotification",
object: nil)
}

func handleCompleteDownload(notification: NSNotification) {
let tmp : [NSObject : AnyObject] = notification.userInfo!

// if notification received, change label value
var id = tmp["fileId"] as! Int!
var current = tmp["fileCurrent"] as! Float!
var total = tmp["fileTotal"] as! Float!
var floatCounter = 100 * current / total
var progressCounter = String(format: "%.f", floatCounter)

if(id == self.fileId){
let priority = DISPATCH_QUEUE_PRIORITY_DEFAULT
dispatch_async(dispatch_get_global_queue(priority, 0)) {
// do some task
dispatch_async(dispatch_get_main_queue()) {
// update some UI
self.downloadLbl.text = "Downloaded \(progressCounter)%"
self.progressBar.setProgress((progressCounter as NSString).floatValue, animated: true)
}
}
}
}

hope that will help!

How does Dispatch.main.async update the UI?

FYI the reason that all of the UI code needs to go on the main thread is because drawing is a (relatively in CPU time) long and expensive process involving many data structures and millions of pixels. The graphics code essentially needs to lock a copy of all of the UI resources when its doing a frame update, so you cannot edit these in the middle of a draw, otherwise you would have wierd artifacts if you went and changed things half way through when the system is rendering those objects. Since all the drawing code is on the main thread, this lets he system block main until its done rendering, so none of your changes get processed until the current frame is done. Also since some of the drawing is cached (basically rendered to texture until you call something like setNeedsDisplay or setNeedsLayout) if you try to update something from a background thread its entirely possible that it just won't show up and will lead to inconsistent state, which is why you aren't supposed to call any UI code on the background threads.

Why private queue dispatch _async update UI after main queue update UI?

Well, according to the docs usually it is best to keep code that manipulates UI on the main thread:

Threads and Your User Interface

If your application has a graphical user interface, it is recommended that you receive user-related events and initiate interface updates from your application’s main thread. This approach helps avoid synchronization issues associated with handling user events and drawing window content. Some frameworks, such as Cocoa, generally require this behavior, but even for those that do not, keeping this behavior on the main thread has the advantage of simplifying the logic for managing your user interface.

Now in your specific case it might be exactly the reason why happens what happens for you. But anyway, in your particular case I see no good reason to perform drawing on a private queue. But there are reasons why you should keep it on main thread - the code manipulates UI, so it is recommended to keep it on main thread (see reference above). Moreover, the point drawing is NOT a performance exhaustive operation - there is no good reason to put it on background.

So I recommend removing that dispatch on private queue and simply use this:

CGPoint touchPoint = [sender locationInView:self.view];
[pencilLayer[0] addPoint:touchPoint];
for(int i = 1; i < 4; i++)
{
CGPoint point = Rotatepoint(500, 500, 45(degree), touchPoint);
[pencilLayer[i] addPoint:point];
}

Also, check this blog entry about threading and UI.

Best/most common way to update UI from background thread

This is the way to go:

let priority = DISPATCH_QUEUE_PRIORITY_DEFAULT
dispatch_async(dispatch_get_global_queue(priority, 0)) {
// do some background task
dispatch_async(dispatch_get_main_queue()) {
// update some UI
}
}


Related Topics



Leave a reply



Submit