Swift Threading: When to use DispatchQueue.main.async? - swift

I believe I understand what the dispatch queue is doing when I call it, but I'm not sure when exactly I should use it and what it's advantages are when I do use it.
If my understanding is correct, DispatchQueue.main.async { // code } will schedule the code contained within the closure to run on the main dispatch queue in an asynchronous manner. The main queue has the highest priority, and is typically reserved for updating UI to maximize App responsiveness.
Where I'm confused is: What exactly is the difference in updating UI elements within a dispatch queue closure versus just writing the code outside the closure in the same spot? Is it faster to execute the code in the body of a view did load method rather than sending it to the dispatch queue? If not, why?
Code Example:
class MyViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
updateUI()
}
}
Versus:
class MyViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
DispatchQueue.main.async {
updateUI()
}
}
}
Which one is will update the UI faster?

The primary use of DispatchQueue.main.async is when you have code running on a background queue and you need a specific block of code to be executed on the main queue.
In your code, viewDidLoad is already running on the main queue so there is little reason to use DispatchQueue.main.async.
But isn't necessarily wrong to use it. But it does change the order of execution.
Example without:
class MyViewController: UIViewController {
func updateUI() {
print("update")
}
override func viewDidLoad() {
super.viewDidLoad()
print("before")
updateUI()
print("after")
}
}
As one might expect, the output will be:
before
update
after
Now add DispatchQueue.main.async:
class MyViewController: UIViewController {
func updateUI() {
print("update")
}
override func viewDidLoad() {
super.viewDidLoad()
print("before")
DispatchQueue.main.async {
updateUI()
}
print("after")
}
}
And the output changes:
before
after
update
This is because the async closure is queued up to run after the current runloop completes.

I just ran into the exact situation discribed in your Question: viewDidLoad() calling DispatchQueue.main.async.
In my case I was wanting to modify Storyboard defaults prior to displaying a view.
But when I ran the app, the default Storyboard items were momentarily displayed. The animated segue would finish. And only THEN would the UI components be modified via the code in viewDidLoad(). So there was this annoying flash of all of the default storyboard values before the real values were edited in.
This was because I was modifying those controls via a helper function that always first dispatched to the main thread. That dispatch was too late to modify the controls prior to their first display.
So: modify Storyboard UI in viewDidLoad() without dispatching to the Main Thread. If you're already on the main thread, do the work there. Otherwise your eventual async dispatch may be too late.

Related

Swift - stopAnimating() - must be used from main thread only

I have a succession of tasks that have to be performed in sequence, so they are corralled by DispatchQueue. To inform the user that they are chugging away I start the activity icon. But how do I stop if off the main thread?
Logically what I am trying to achieve is:
override func viewDidLoad() {
super.viewDidLoad()
self.activityIcon.startAnimating()
DispatchQueue.main.async {
self.bigTask1()
}
DispatchQueue.main.async {
self.bigTask2()
self.activityIcon.stopAnimating()
}
}
This obviously generates the runtime error: " UIActivityIndicatorView.stopAnimating() must be used from main thread only".
Putting the stopAnimating() in the main queue turns it on and off so fast no one will ever see it.
What is the approved way call functions like this off the main queue?
Many thanks. P.s. I have read answers of similar questions on SO but don't quite get them.
You can do big task in default background queue, and when the big task completes then simply get the main queue and perfom any UI Updates.
override func viewDidLoad() {
super.viewDidLoad()
self.activityIcon.startAnimating()
DispatchQueue.global().async {
self.someBigTask()
DispatchQueue.main.async {
self.activityIcon.stopAnimating()
}
}
}
Hope you will find this helpful.

NSEvent leak for key down in macOS

In Xcode 10.1 with Swift 4.2 I'm having a memory leak when I add a local monitor for key down events in my NSViewController, that it is be instanced as minimal version (without nib and xib).
override func loadView() {
self.view = NSView()
self.view.wantsLayer = true
}
override func viewDidLoad(){
super.viewDidLoad
NSEvent.addLocalMonitorForEvents(matching: .keyDown, handler: handler)
}
lazy var handler:(NSEvent)->NSEvent? = { [ weak self ,unowned picker = picker] event in
picker.keyDown(with: event)
return event
}
This memory leak does not have much information:Memory leak
EDIT
In deinit method removeMonitor is called
deinit {
NSEvent.removeMonitor(self)
}
EDIT 2
Issue solved :
override func loadView() {
self.view = NSView()
self.view.wantsLayer = true
}
var monitor:Any? // This is essential
override func viewDidLoad(){
super.viewDidLoad
monitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown, handler: handler)
}
lazy var handler:(NSEvent)->NSEvent? = { [ weak self ,unowned picker = picker] event in
picker.keyDown(with: event)
return event
}
deinit {
NSEvent.removeMonitor(monitor)
}
From the Apple Docs;
Note
The monitor Block is called for all future events that match mask. You must call removeMonitor(_:) to stop the monitor. Under garbage collection, the monitor (and everything the Block references) will not be collected until removeMonitor(_:) is invoked.
Meaning that the monitor will continue to look for matching events until removeMonitor() is invoked. So your system is using extra memory to keep looking for events, and if you never call this - it could lead to a fairly large memory leak. As it says even with garbage collection, this object is still allocated - because it is looking for events that could take place at any time (so it is not guaranteed that this will be collected). Make sure you call this when you want the system to stop looking for events.
You could also do something like this in your handler.
You can return the event unmodified, create and return a new NSEvent object, or return nil to stop the dispatching of the event.

How to access widget state outside of main thread

I need to read a checkbox’s state from a thread that is not the main thread. It seems I cannot simply use the following (which causes the Main Thread Checker to say “UI API called on a background thread: -[NSCell state]”):
myCheckbox.state
What am I supposed to do instead?
My current solution is to maintain a property that gets updated when the checkbox gets switched, and access that property instead of directly reading the checkbox’s state.
Something more elegant would be welcome: this is cumbersome; even more so if you want to make the property read-only or properly handle setting its value from inside the program.
class myViewController: NSViewController {
#IBOutlet private weak var myCheckbox: NSButtonCell!
public var myCheckboxState: Bool! // This can be read from any thread.
#IBAction func onMyCheckboxAction(_ sender: NSButtonCell) {
myCheckboxState = (myCheckbox.state == NSOnState)
}
override func viewDidLoad() {
super.viewDidLoad()
...
myCheckboxState = (myCheckbox.state == NSOnState)
...
}
...
}

What's causing this? - "CoreAnimation: warning, deleted thread with uncommitted CATransaction"

Xcode 8.2.1 / macOS 10.12.4 beta
I'm trying to understand an error I'm getting:
CoreAnimation: warning, deleted thread with uncommitted CATransaction; set CA_DEBUG_TRANSACTIONS=1 in environment to log backtraces, or set CA_ASSERT_MAIN_THREAD_TRANSACTIONS=1 to abort when an implicit transaction isn't created on a main thread.
I have a block of code in a swift file that sends a network request, retrieves some data and processes it before posting a notification:
// global variable
var data: [String: Any] = [:]
func requestData() {
...
do {
// process data
data = processedData
NotificationCenter.default.post(name: didProcessData, object: nil)
}
} catch {
...
}
In my main ViewController.swift file, an observer listens for the notification and updates the view.
func updateView() {
// update the view
textField.stringValue = "..."
}
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(updateView), name: didProcessData, object: nil)
}
When updateView() is called, the actual text fields and such take a random amount of time to actually update, and the aforementioned error appears.
My guess was that there was an issue with thread safety, so I changed updateView() like so:
func updateView() {
DispatchQueue.main.async(execute: {
// update the view
self.textField.stringValue = "..."
})
}
And now the view updates properly. But I'm still relatively inexperienced with programming, and I don't quite understand what exactly is causing the error.
Your notification handler (updateView) is called from an arbitrary thread -- e.g. not from the main thread (so-called UI-Thread). Several framework APIs detect when they are called from outside the main thread (as a kind-of warning).
If you want to update the UI, you'll have to make sure that this code is executed in the main thread; this is typically accomplished by enqueuing a closure (work item) to DispatchQueue.main.async, which is part of GCD (Grand Central Dispatch).

How to Complete Async call before app loads?

I couldn't find the answer to this probably because I'm not really sure what I'm looking for since i just started programing a few weeks ago.
My storyboard entry point requires data that I get from an asynchronous session and JSON parse. Then once it gets the data it stores it to NSUserDefaults so it doesn't have to make the async call again and the app can access that data anytime.
I put my async call in the viewdidload of the storyboard entry point because as far as I know thats where the app starts. The issue is that the data isn't showing up until the app is started for a second time.
The data I'm getting from the async call only changes once every month so its not necessarily time sensitive.
How can I delay the app from getting to the storyboard entry point until the async call is finished?
Is that even the right way to go about it?
Should I switch to a synchronous call?
What if I changed the storyboard entry point to a view controller that looked like the app was loading and then when the async call finished, use a completion handler to perform segue to the view controller that requires the asynchronous call to finish?
Thanks Leo that worked.
Storing a variable when the app is first installed
I did two things here. First I created a new view controller that would execute the async call and segue to my main view controller when it finished. Then I detected if it was the first launch or not by using the above linked solution. Both of those together worked.
import Foundation
import UIKit
class FirstLoad: UIViewController {
var installedDate: NSDate? {
get {
return NSUserDefaults().objectForKey("installedDateKey") as? NSDate
}
set {
NSUserDefaults().setObject(newValue, forKey: "installedDateKey")
}
}
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func viewDidAppear(animated: Bool) {
firstLoad()
}
func firstLoad() {
if installedDate == nil {
installedDate = NSDate()
parseData(heroesDataProject) { heroesArrayFromParse in //this function gets my json and the following code is executed after completion
saveToDefaults("heroesOriginal")
print("First Run")
self.performSegueWithIdentifier("firstLoadToHomeMenu", sender: nil)
}
} else {
print("Not first run, installd on \(installedDate!)")
loadFromDefaults(userProfile)
performSegueWithIdentifier("firstLoadToHomeMenu", sender: nil)
}
}
}