I want to do some async processing in my MTKView's draw(_:) method. For that I'm first getting the currentDrawable and then using it inside of a Task:
override func draw(_ rect: CGRect) {
guard let currentDrawable = self.currentDrawable else { return }
Task {
// <image filtering>
let destination = CIRenderDestination(width: Int(drawableSize.width),
height: Int(drawableSize.height),
pixelFormat: self.colorPixelFormat,
commandBuffer: nil,
mtlTextureProvider: { () -> MTLTexture in
return currentDrawable.texture
})
try await asynContext.startTask(toClear: destination)
try await asynContext.startTask(toRender: image, to: destination)
currentDrawable.present()
}
}
While this works, the performance is much worse than using a DispatchQueue or staying on the main thread.
It seems the Task is holding on to the drawable for much longer than its actual execution, which causes the next call to currentDrawable to stall when no more drawables are in the pool at the moment.
I assume the Task itself is still referenced/used by the concurrency runtime after its execution for a little while longer.
Is there a way to prevent that? Or, alternatively, to have the Task's operation block release the drawable early?
As by matt's request, a little more elaboration:
I want to move some computation Core Image is doing on the filter graph into a background queue to not block the main thread. For that, I created a little helper actor:
actor AsyncContext {
private let context: CIContext
init(context: CIContext) {
self.context = context
}
#discardableResult
func startTask(toClear destination: CIRenderDestination) throws -> CIRenderTask {
return try self.context.startTask(toClear: destination)
}
#discardableResult
func startTask(toRender image: CIImage, to destination: CIRenderDestination) throws -> CIRenderTask {
return try self.context.startTask(toRender: image, to: destination)
}
}
The actor should ensure that calls to startTask(...) are happening in a background queue (which they do).
I'm pretty confident that the actor itself is not the problem. It's the Task not getting released in time, I think.
Related
I am following Stanfords' CS193p Developing Apps for iOS online course.
It is using the Grand Central Dispatch (GCD) API for a demo of multithreading.
But they noted, that
"GCD has been mostly replaced by Swift's new built-in async API as of WWDC 2021".
So I wanted to learn how the code from the Lecture would look like after updating it to use this new API.
After watching Apple's WWDC videos, it seems to me like
DispatchQueue.global(qos: .userInitiated).async { } is replaced in this new async API with Task { } or Task(priority: .userInitiated) {}, but I'm not sure, what has DispatchQueue.main.async { } been replaced with?
So, my questions are:
Am I correctly assuming, that DispatchQueue.global(qos: .userInitiated).async { } has been replaced with Task(priority: .userInitiated) {}
What has DispatchQueue.main.async { } been replaced with?
Please help, I want to learn this new async-await API.
Here's the code from the Lecture, using old GCD API:
DispatchQueue.global(qos: .userInitiated).async {
let imageData = try? Data(contentsOf: url)
DispatchQueue.main.async { [weak self] in
if self?.emojiArt.background == EmojiArtModel.Background.url(url) {
self?.backgroundImageFetchStatus = .idle
if imageData != nil {
self?.backgroundImage = UIImage(data: imageData!)
}
// L12 note failure if we couldn't load background image
if self?.backgroundImage == nil {
self?.backgroundImageFetchStatus = .failed(url)
}
}
}
}
The whole function (in case you need to see more code):
private func fetchBackgroundImageDataIfNecessary() {
backgroundImage = nil
switch emojiArt.background {
case .url(let url):
// fetch the url
backgroundImageFetchStatus = .fetching
DispatchQueue.global(qos: .userInitiated).async {
let imageData = try? Data(contentsOf: url)
DispatchQueue.main.async { [weak self] in
if self?.emojiArt.background == EmojiArtModel.Background.url(url) {
self?.backgroundImageFetchStatus = .idle
if imageData != nil {
self?.backgroundImage = UIImage(data: imageData!)
}
// L12 note failure if we couldn't load background image
if self?.backgroundImage == nil {
self?.backgroundImageFetchStatus = .failed(url)
}
}
}
}
case .imageData(let data):
backgroundImage = UIImage(data: data)
case .blank:
break
}
}
If you really are going to do something slow and synchronous, Task.detached is a closer analog to GCD’s dispatching to a global queue. If you just use Task(priority: ...) { ... } you are leaving it to the discretion of the concurrency system to decide which thread to run it on. (And just because you specify a lower priority does not guarantee that it might not run on the main thread.)
For example:
func fetchAndUpdateUI(from url: URL) {
Task.detached { // or specify a priority with `Task.detached(priority: .background)`
let data = try Data(contentsOf: url)
let image = UIImage(data: data)
await self.updateUI(with: image)
}
}
And if you want to do the UI update on the main thread, rather than dispatching it back to the main queue, you would simply add the #MainActor modifier to the method that updates the UI:
#MainActor
func updateUI(with image: UIImage?) async {
imageView.image = image
}
That having been said, this is a pretty unusual pattern (doing the network request synchronously and creating a detached task to make sure you don't block the main thread). We would probably use URLSession’s new asynchronous data(from:delegate:) method to perform the request asynchronously. It offers better error handling, greater configurability, participates in structured concurrency, and is cancelable.
In short, rather than looking for one-to-one analogs for the old GCD patterns, use the concurrent API that Apple has provided where possible.
FWIW, in addition to the #MainActor pattern shown above (as a replacement for dispatching to the main queue), you can also do:
await MainActor.run {
…
}
That is roughly analogous to the dispatching to the main queue. In WWDC 2021 video Swift concurrency: Update a sample app, they say:
In Swift’s concurrency model, there is a global actor called the main actor that coordinates all operations on the main thread. We can replace our DispatchQueue.main.async with a call to MainActor’s run function. This takes a block of code to run on the MainActor. …
But he goes on to say:
I can annotate functions with #MainActor. And that will require that the caller switch to the main actor before this function is run. … Now that we've put this function on the main actor, we don’t, strictly speaking, need this MainActor.run anymore.
I am trying to create a Robotic Process Automation tool for Macos using Swift. Users create an Automation that is an array of Step objects and then play it. One of the subclasses of Step is Pause which is supposed to pause the execution for a given number of seconds.
For some reason, execution hangs when I use the DispatchQueue.main.asyncAfter() method in the Pause class. Usually the first run through the automation is fine, but when it goes to repeat, it eventually hangs for much longer. The error goes away when I use sleep() instead.
The other weird thing about this bug is when I open Xcode to try and see what is happening, the hang resolves and execution continues. I am wondering if the process enters background somehow and then the DispatchQueue.main.asyncAfter() doesn't work. I have tried to change the Info.plist "Application does not run in background" to YES, but this doesn't have any effect.
The problem with using sleep() is it blocks the UI thread so users can't stop the automation if they need to. I have tried lots of different variations of threading with DispatchQueue, but it always seems to hang somewhere on repeat execution. I have also tried using a Timer.scheduledTimer() instead of DispatchQueue but that hangs as well. I'm sure I'm missing something simple, but I can't figure it out.
Creating the Step Array and Starting Automation
class AutomationPlayer {
static let shared = AutomationPlayer()
var automation: Automation?
var stepArray: [Step] = []
func play() {
// Create array of steps
guard let steps = automation?.steps, let array = Array(steps) as? [Step] else {
return
}
// If the automation repeats, add more steps to array.
for _ in 0..<(automation?.numberOfRepeats ?? 1) {
for (index, step) in array.enumerated() {
stepArray.append(step)
}
}
// Add small delay to allow window to close before execution.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { [weak self] in
self?.execute(index: 0)
}
}
private func execute(index: Int) {
let step = stepArray[index]
executeStep(step: step) { [weak self] success, error in
guard error == nil else { return }
let newIndex = index + 1
if newIndex < self?.stepArray.count ?? 0 {
//Need a small delay between steps otherwise execution is getting messed up.
usleep(400000)
self?.execute(index: newIndex)
} else {
self?.stepArray = []
}
}
}
private func executeStep(step: Step?, completionHandler: #escaping (Bool, Error?) -> Void) -> Void {
step?.execute(completionHandler: { [weak self] success, error in
guard error == nil else {
completionHandler(false, error)
return
}
completionHandler(true, nil)
})
}
Pause Class
#objc(Pause)
public class Pause: Step {
override func execute(completionHandler: #escaping (Bool, Error?) -> Void) {
print("Pause for: \(self.time) seconds")
// This will eventually hang when the automation repeats itself
DispatchQueue.main.asyncAfter(deadline: .now() + Double(self.time)) {
completionHandler(true, nil)
})
// This will also hang
Timer.scheduledTimer(withTimeInterval: self.time, repeats: false) { timer in
completionHandler(true, nil)
}
// If I use this instead, the automation repeats just fine
sleep(UInt32(self.time))
completionHandler(true, nil)
}
}
So I think I figured it out. MacOS was putting my app into AppNap after a certain period of time which would cause the DispatchQueue.main.async() to stop working. For some reason, AppNap does not affect delays when you use sleep()
I found an answer here
This answer was a little older. I am using SwiftUI to build my mac app so I added this my #main struct
#main
struct Main_App: App {
#State var activity: NSObjectProtocol?
var body: some Scene {
WindowGroup("") {
MainWindow()
.onAppear {
activity = ProcessInfo().beginActivity(options: .userInitiated, reason: "Good Reason")
}
}
}
This seems to prevent the app from going into AppNap and the automation continues. It's pretty ugly, but it works.
Does promises always run on background thread.
#IBAction func doNetworkCall(_ sender: Any) { // Does this run on
background thread
Network.fetchPhotos().done { (photos) in
}
}
static func fetchPhotos () -> Promise<[Photo]> {
return Promise { seal in
AF.request("https:photosURL", method: .post, parameters: ["auth":"1231","user_id":"u12312"]).responseJSON { (response) in
guard let data = response.data else { return }
let coder = JSONDecoder()
let photos = try! coder.decode([Photo].self, from: data)
seal.fulfill(photos)
}
}
}
I have used promises with purpose of all network calls runs on background thread irrespective calling from main thread. I have some chain network requests which will be easier to implement.
is this assumption correct?
Alamofire always runs its requests on a background queue. In your example the only part that isn't in the background is the responseJSON closure. By default that closure runs on the .main queue. I recommend you adopt responseDecodable to decode your responses so that the parsing is also in the background only call fulfill the promise in the closure. (I'm not sure whether fulfilling on the main queue is otherwise necessary.)
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'm using HanekeSwift to retrieve cached data and then set it to labels in a swipeView every time the view appears. My code retrieves the data no problem, but because cache.fetch() is asynchronous, when I call my method to update the view, my labels are set to nil. Is there anyway to tell swift to wait until my cached data is retrieved before loading the view?
See code below:
override func viewWillAppear(animated: Bool) {
updateEntries() // updates entries from cache when view appears
}
func updateEntries() {
guard let accessToken = NSUserDefaults.standardUserDefaults().valueForKey("accessToken") as? String else { return }
guard let cachedEntryKey = String(accessToken) + "food_entries.get" as? String else { return }
cache.fetch(key: cachedEntryKey).onSuccess { data in
...
// if successful, set labels in swipeView to data retrieved from cache
...
dispatch_group_leave(dispatchGroup)
} .onFailure { error in
print(error)
...
// if unsuccessful, call servers to retrieve data, set labels in swipeView to that data
...
dispatch_group_leave(dispatchGroup)
}
}
When I step through the above code, it always displays the view and then steps into the cache block. How do I make viewWillAppear() allow updateEntries() to complete and not return out of it until the cache block is executed? Thanks a ton in advance!
Update 1:
The solution below is working pretty well and my calls are made in the correct sequence (my print statement in the notify block executes after the cache retrieval), but my views only update their labels with non-nil values when the server is called. Maybe I'm lumping the wrong code in the notify group?
override func viewWillAppear(animated: Bool) {
self.addProgressHUD()
updateEntries() // updates entries from cache when view appears
}
func updateEntries() {
guard let accessToken = NSUserDefaults.standardUserDefaults().valueForKey("accessToken") as? String else { return }
guard let cachedEntryKey = String(accessToken) + "food_entries.get" as? String else { return }
let dispatchGroup = dispatch_group_create()
dispatch_group_enter(dispatchGroup)
dispatch_group_async(dispatchGroup, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
cache.fetch(key: cachedEntryKey).onSuccess { data in
...
// if successful, set labels in swipeView to data retrieved from cache
...
} .onFailure { error in
print(error)
...
// if unsuccessful, call servers to retrieve data, set labels in swipeView to that data
...
}
}
dispatch_group_notify(dispatchGroup, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
print("Retrieved Data")
self.removeProgressHUD()
}
}
Update 2:
Also, I'm getting this warning in the console when I switch views. I think I'm locking up the main thread with the above code
"This application is modifying the autolayout engine from a background thread, which can lead to engine corruption and weird crashes. This will cause an exception in a future release."
Note:
enter group before calling asynchronous method
leave group is each of the respective completion/failure handlers
dispatch UI updates in notify block to main queue
Thus:
func updateEntries() {
guard let accessToken = NSUserDefaults.standardUserDefaults().valueForKey("accessToken") as? String else { return }
guard let cachedEntryKey = String(accessToken) + "food_entries.get" as? String else { return }
let group = dispatch_group_create()
dispatch_group_enter(group)
cache.fetch(key: cachedEntryKey).onSuccess { data in
...
// if successful, set labels in swipeView to data retrieved from cache
...
dispatch_group_leave(group)
} .onFailure { error in
print(error)
...
// if unsuccessful, call servers to retrieve data, set labels in swipeView to that data
...
dispatch_group_leave(group)
}
dispatch_group_notify(group, dispatch_get_main_queue()) {
print("Retrieved Data")
self.removeProgressHUD()
}
}
Ok suggestions from everyone helped a ton on this. Think I got it. I need to make sure my cache block isn't blocking the main queue. See code below
EDIT
Thanks to #Rob for helping me make the proper adjustments to make this work
let dispatchGroup = dispatch_group_create()
dispatch_group_enter(dispatchGroup)
cache.fetch(key: cachedEntryKey).onSuccess { data in
...
// if successful, set labels in swipeView to data retrieved from cache
...
dispatch_group_leave(dispatchGroup)
} .onFailure { error in
print(error)
...
// if unsuccessful, call servers to retrieve data, set labels in swipeView to that data
...
dispatch_group_leave(dispatchGroup)
}
dispatch_group_notify(dispatchGroup, dispatch_get_main_queue()) {
print("Retrieved Data")
self.removeProgressHUD()
}
Here's simple example that you can stage a loading screen. I just create a alert view, also you can create your custom loading indicator view instead.
let alert = UIAlertController(title: "", message: "please wait ...", preferredStyle: .alert)
override func viewWillAppear(animated: Bool) {
self.present(alert, animated: true, completion: nil)
updateEntries() // updates entries from cache when view appears
}
func updateEntries() {
guard let accessToken = UserDefaults.standard.value(forKey: "accessToken") as? String,
let cachedEntryKey = (accessToken + "food_entries.get") as? String else {
return
}
cache.fetch(key: cachedEntryKey).onSuccess { data in
...
// update value in your UI
alert.dismiss(animated: true, completion: nil)
...
} .onFailure { error in
print(error)
...
// if unsuccessful, call servers to retrieve data, set labels in swipeView to that data
...
}
}
While I entirely agree with #ozgur about displaying some sort of loading indicator from a UX standpoint, I figured the benefit of learning how to use Grand Central Dispatch (Apple's native solution to asynchronous waiting) might help you in the long-term.
You can use dispatch_groups to wait for a block(s) of code to completely finish running before running a completion handler of some sort.
From Apple's documentation:
A dispatch group is a mechanism for monitoring a set of blocks. Your application can monitor the blocks in the group synchronously or asynchronously depending on your needs. By extension, a group can be useful for synchronizing for code that depends on the completion of other tasks.
[...]
The dispatch group keeps track of how many blocks are outstanding, and GCD retains the group until all its associated blocks complete execution.
Here's an example of dispatch_groups in action:
let dispatchGroup = dispatch_group_create()
dispatch_group_async(dispatchGroup, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
// Run whatever code you need to in here. It will only move to the final
// dispatch_group_notify block once it reaches the end of the block.
}
dispatch_group_notify(dispatchGroup, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
// Code in here only runs once all dispatch_group_async blocks associated
// with the dispatchGroup have finished completely.
}
The great part about dispatch_groups are that they allow you to run multiple asynchronous blocks at the same time and wait for all of them to finish before running the final completion handler. In other words, you can associate as many dispatch_group_async blocks with the dispatchGroup as you want.
If you wanted to go for the loading indicator approach (which you should), you can run code to display the loading indicator, then move into a dispatch_group with a completion handler to remove the loading indicator and load data into view once the dispatch_group completes.