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.
Related
I have a search page with uses OAuth to make an external call to a website for data. Sometimes the data is very quick and others quite long. So I have created a custom object (Searching) to display on screen to indicate that a search is happening (the custom object is just 2 UIImageViews in a UIView)
The problem is that the searching.isHidden = false doesn't actually happen until the end of the func which happens after it gets all the data, even though it is called first. Which is obviously too late.
I tried moving the isHidden to a background thread but get an error saying UIView calls must be on the main thread
I tried moving the display call to its own func with an #escaping callback and then run the search after it returns but it still does not update.
If I remove the search() line it displays properly.
I've also tried forcing a refresh on the object using
self.view.setNeedsLayout()
self.view.setNeedsDisplay()
and it didn't work
class Search {
override func viewDidLoad() {
super.viewDidLoad()
searching.isHidden = true
}
#IBAction func search(_ sender: Any) {
if self.searching.isHidden == false {
self.searching.isHidden = true
}
else {
self.searching.isHidden = false
}
self.view.setNeedsLayout()
self.view.setNeedsDisplay()
//I've also tried using an escaping func to call the isHidden and call back when complete
//self.searching.show() {
//self.view.setNeedsLayout()
//self.view.setNeedsDisplay()
//self.search()
//}
//I've tried an async call
// DispatchQueue.main.async {
// self.search()
// }
}
func search() {
keywordText.resignFirstResponder()
//perform OAuth external search
if results.count > 1 {
searching.isHidden = true
self.performSegue(withIdentifier: "results", sender: nil)
}
return
}
}
On iOS (and MacOS) UI updates don’t get displayed until your code returns and the event loop gets a chance to run. Code that makes UI changes and then immediately does a long-running task on the main thread will not see those changes on-screen until the long-running task completes.
One way to handle this is to change you UI and then use dispatchAsync to trigger the long-running task:
searching.isHidden = false
DispatchQueue.main.async {
// Put your long-running code here.)
}
During a computationally intensive task, I wish to show the user an activity indicator. What is the best way to do this?
My task (contrived of course), lasts a couple of seconds:
func startThinking(howMany: Int) {
for i in 0...howMany {
let p:Double = Double(i)
let _ = p / Double.pi
}
delegate?.finishedThinking()
}
This is called on a button tap:
#IBAction func startTap(_ sender: Any) {
Thinker.sharedInstance.startThinking(howMany: 500000000)
myActivity.startAnimating()
}
And stopped when the thinking task is finished:
func finishedThinking() {
print ("finished thinking")
myActivity.stopAnimating()
}
But the activity indicator is not showing up; the UI is blocked by the difficult thinking task.
I've tried putting the startAnimating on the main thread:
DispatchQueue.main.async {
self.myActivity.startAnimating()
}
or the difficult task onto its own thread:
DispatchQueue.global().async {
Thinker.sharedInstance.startThinking(howMany: 500000000)
}
and various other combinations that I've run across in Stack. What am I doing wrong?
Firstly, I would move the call to start animating to before the thinker call, and verify that it works if you don't start thinking. You also need to stop the animation from the main thread.
#IBAction func startTap(_ sender: Any) {
myActivity.startAnimating()
DispatchQueue.global(qos: .userInitiated).async {
Thinker.sharedInstance.startThinking(howMany: 500000000)
}
}
func finishedThinking() {
DispatchQueue.main.async {
myActivity.stopAnimating()
}
}
I adjusted a few things:
moved the .startAnimating() call to be first. It is already on the main thread since it was called from the interface
specify the qos as .userInitiated
run the .stopAnimating() on the main thread
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.
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).
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)
}
}
}