Heavy Duty Work With Async Await

I am perplexed as to how to use async await. In the following example, I don't use GCD or performSelector(inBackground:with:). The view controller is NSViewController, but it doesn't make any difference if it's NSViewController or UIViewController.

import Cocoa

class ViewController: NSViewController {
	func startWriteImages() {
		Task{
			let bool = await startWriteImagesNext()
			if bool {
				print("I'm done!")
			}
		}
	}
	
	func startWriteImagesNext() async -> Bool {
		// pictures is a path to a folder in the sandbox folder
		// appDelegate.defaultFileManager is a variable pointing to FileManager.default in AppDelegate
		let pictURL = URL(fileURLWithPath: pictures)
		if let filePaths = try? self.appDelegate.defaultFileManager.contentsOfDirectory(atPath: pictURL.path) {
			for file in filePaths {
				let fileURL = pictURL.appending(component: file)
				if self.appDelegate.defaultFileManager.fileExists(atPath: fileURL.path) {
					let newURL = self.folderURL.appending(component: file)
					do {
						try self.appDelegate.defaultFileManager.copyItem(at: fileURL, to: newURL)
					} catch {
						print("Ugghhh...")
					}
				}
			}
			return true
		}
		return false
	}
	
	func startWriteImagesNext2() async -> Bool {
		let pictURL = URL(fileURLWithPath: pictures)
		if let filePaths = try? self.appDelegate.defaultFileManager.contentsOfDirectory(atPath: pictURL.path) {
			DispatchQueue.global().async() {
				for file in filePaths {
					let fileURL = pictURL.appending(component: file)
					if self.appDelegate.defaultFileManager.fileExists(atPath: fileURL.path) {
						let newURL = self.folderURL.appending(component: file)
						do {
							try self.appDelegate.defaultFileManager.copyItem(at: fileURL, to: newURL)
						} catch {
							print("Ugghhh...")
						}
					}
				}
			}
			return true
		}
		return false
	}
}

In the code above, I'm saving each file in the folder to user-selected folder (self.folderURL). And the application will execute the print guy only when work is done. Since it's heavy-duty work, I want to use CCD or performSelector(inBackground:with:). If I use the former (startWriteImagesNext2), the application will execute the print guy right at the beginning. I suppose I cannot use GCD with async. So how can I perform heavy-duty work? Muchos thankos.

Replies

The non-obvious thing that's catching you here is that your task is running on the main actor (automatic with any UIViewController or NSViewController). To put work onto a background queue from the main actor, use Task.detached.

  • Thanks. I have done so, actually, with no vail. With or without priority set to .background, .high, or .low, it doesn't help.

  • Task.detached is a bad idea

Add a Comment

As a general rule I recommend that you avoid doing async work in your view controllers. That’s because your view controllers are bound to the main thread / queue / actor, and that causes one problem or another.

It’s better to separate this work into some sort of model-level controller. So, you’ve view controller packages up the work into some job, passes that to that model-level controller, which does the work, and then applies the changes to your model, which then updates your view controller. In your case:

  • The work to do is simply that pictures URL.

  • And the model value that updates is just a ‘we are busy’ Boolean.

Now, how you actually do this depends on the concurrency model you’re planning to use. If you want to use Swift concurrency, that model-level controller is just an actor.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

  • Hello, Eskimo. Thank you for your advice. So are you saying that it's okay for the application to have the rainbow wheel stick around while it's doing heavy duty work? I do show a 'Please wait' label over the window sheet. I have an abort button that the user can click on to abort the process. I guess the abort button is needed, then.

Add a Comment

You need to mark the func as nonisolated if you want a background thread, e.g.

nonisolated func startWriteImagesNext() async -> Bool {

Because you defined it in a ViewController it inherited the @MainActor which doesn't seem what you want to nonisolated bypasses that. Check the NSViewController/UIViewController header to see its @MainActor annotation.

You could also declare the func outside of the ViewController to achieve the same effect, e.g. in a struct that is not marked as @MainActor, or a actor if you want some shared state between all the image tasks.

So are you saying that it's okay for the application to have the rainbow wheel stick around while it's doing heavy duty work?

No. Your UI must remain active and, ideally, it’d support progress and cancellation. How you achieve that depends on the concurrency model you choose to adopt. If you want to use Swift concurrency, do that work in an actor.

The issue you’re having is that, by doing your work in your view controller, it ends up bound to the main actor. If you move the work to an actor that you create, that breaks that binding and your UI will stay responsive.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

  • Thank you, eskimo. I think I understand your point.

Add a Comment