How to Make Async/Await in Swift

How to make async / await in Swift?

Thanks to vadian's comment, I found what I expected, and it's pretty easy. I use DispatchGroup(), group.enter(), group.leave() and group.notify(queue: .main){}.

func myFunction() {
let array = [Object]()
let group = DispatchGroup() // initialize

array.forEach { obj in

// Here is an example of an asynchronous request which use a callback
group.enter() // wait
LogoRequest.init().downloadImage(url: obj.url) { (data) in
if (data) {
group.leave() // continue the loop
}
}
}

group.notify(queue: .main) {
// do something here when loop finished
}
}

How to make an async Swift function @synchronized?

You can have every Task await the prior one. And you can use actor make sure that you are only running one at a time. The trick is, because of actor reentrancy, you have to put that "await prior Task" logic in a synchronous method.

E.g., you can do:

actor Experiment {
private var previousTask: Task<Void, Error>?

func startSomethingAsynchronous() {
previousTask = Task { [previousTask] in
let _ = await previousTask?.result
try await self.doSomethingAsynchronous()
}
}

private func doSomethingAsynchronous() async throws {
let id = OSSignpostID(log: log)
os_signpost(.begin, log: log, name: "Task", signpostID: id, "Start")
try await Task.sleep(nanoseconds: 2 * NSEC_PER_SEC)
os_signpost(.end, log: log, name: "Task", signpostID: id, "End")
}
}

Now I am using os_signpost so I can watch this serial behavior from Xcode Instruments. Anyway, you could start three tasks like so:

import os.log

private let log = OSLog(subsystem: "Experiment", category: .pointsOfInterest)

class ViewController: NSViewController {

let experiment = Experiment()

func startExperiment() {
for _ in 0 ..< 3 {
Task { await experiment.startSomethingAsynchronous() }
}
os_signpost(.event, log: log, name: "Done starting tasks")
}

...
}

And Instruments can visually demonstrate the sequential behavior (where the shows us where the submitting of all the tasks finished), but you can see the sequential execution of the tasks on the timeline:

Sample Image


I actually like to abstract this serial behavior into its own type:

actor SerialTasks<Success> {
private var previousTask: Task<Success, Error>?

func add(block: @Sendable @escaping () async throws -> Success) {
previousTask = Task { [previousTask] in
let _ = await previousTask?.result
return try await block()
}
}
}

And then the asynchronous function for which you need this serial behavior would use the above, e.g.:

class Experiment {
let serialTasks = SerialTasks<Void>()

func startSomethingAsynchronous() async {
await serialTasks.add {
try await self.doSomethingAsynchronous()
}
}

private func doSomethingAsynchronous() async throws {
let id = OSSignpostID(log: log)
os_signpost(.begin, log: log, name: "Task", signpostID: id, "Start")
try await Task.sleep(nanoseconds: 2 * NSEC_PER_SEC)
os_signpost(.end, log: log, name: "Task", signpostID: id, "End")
}
}

Swift Concurrency UIButton

The crash is unrelated to your URLSession code. The problem is that you have declared the button’s selector to be asynchronous throwing function. That is incompatible with an @objc selector.

Replace:

@objc func testAsync() async throws { 
...
}

With:

@objc func didTapButton(_ sender: Any) {
Task {
try await testAsync()
}
}

func testAsync() async throws {
...
}

The key observation is that the @objc method is not an asynchronous function. I would also give the button handler a sender parameter and a more descriptive name, to follow established conventions and make it clear at a glance that it is handling a button tap.

Obviously, when you do this, you will have to change the #selector reference, too:

button.addTarget(self, action: #selector(didTapButton(_:)), for: .touchUpInside)

How can I use async/await with SwiftUI in Swift 5.5?

As per new informations in WWDC session Meet async/await in Swift
WWDC21, at 23m:28s this is now done using:

Task {
someState = await someAsyncFunction()
}

Screenshot from the session.

Image showing how to use an Async task on a sync context

Note that there are more ways to instantiate a task. This is the most basic. You can use Task.detached as well and both ways can get a priority argument.
Check both the Task docs and the session
Check Explore structured concurrency in Swift WWDC21 at around 23:05 (I recommend the whole session!) for more info.

How to pause an asynchronous Swift function until a callback is called when a result is available from another library(JavaScriptCore)

Okay, as it turns out, Swift async/await concurrency does have a support for continuation. It's not mentioned on the main article on Swift documentation and most 3rd party articles bill this feature as a way to integrate the old callback centric concurrency with the new async/await API, however this is much more useful than simply integrating the old code with the new one.

Continuation provides you with an asynchronous function that can be called from within your async function to pause execution with await until you explicitly resume it. That means, you can await input from the user or another library that uses callback to deliver the results. That also means, you are entirely responsible to correctly resume the function.

In my case, I'm providing the JavaScriptCore library with a callback that resumes execution from Swift, which is called by an async JavaScript code upon completion.

Let me show you. This is a JS code that asynchronously processes a request:

async function processRequest(data, callback){
try {
let result = await myAsyncJSProcessing(data)
callback(result, null)
} catch(err) {
callback(null, err)
}
}

To be able to pause the Swift execution until the JS code finishes processing and continue once the JS calls the callback with the data, I can use continuation like this:

//proccessRequestSync is a synchronous Swift function that provides the callback that the JS will call.
// This is also the function with a callback that Swift will wait until the callback is called
// self.engine is the initiated JavaScriptCore context where the JS runs.
// We create the callback that we will pass to the JS engine as JSValue, then pass it to the JS function as a parameter. Once the JS is done, calls this function.
func proccessRequestSync(_ completion : @escaping (Result<JSValue, Error>) -> Void){
let callback : @convention(block) (JSValue, JSValue) -> Void = { success, failure in
completion(.success(success))
}
let jsCallback = JSValue(object: callback, in: self.engine)
self.engine?.objectForKeyedSubscript("processRequest").call(withArguments: ["some_data", jsCallback])
}
// Then we create the async function that will be paused using continuation.
// We can integrate the function into the rest of our async Swift.
func proccessRequestAsync() async throws -> JSValue {
//Continuation happens right here.
return try await withCheckedThrowingContinuation({ continuation in
proccessRequestSync { result in
// This is the closure that will be called by the JS once processing is finished
// We will explicitly resume the execution of the Swift code by calling continuation.resume()
switch result {
case .success(let val):
continuation.resume(returning: val)
case .failure(let error):
continuation.resume(throwing: error)
}
}
})
}

if let result = try? await proccessRequestAsync()

How do you execute code when scene goes to background using new Swift async/await concurrency model?

This pattern works fine. But the print statements make me suspect that you must be attempting to watch this in the Xcode debugger console. But when testing background lifecycle events, you do not want to be attached to the Xcode debugger, as that artificially changes the app lifecycle.

That begs the question as to how one would watch for logging messages while not connected to Xcode. Personally, I use the unified logging system, namely Logger. For example:

import SwiftUI
import os.log

private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "MyAppApp")

@main
struct MyAppApp: App {
@Environment(\.scenePhase) var scenePhase

var body: some Scene {
WindowGroup {
ContentView()
.onChange(of: scenePhase) {
guard $0 == .background else { return }

logger.debug("starting background task")

var bgTaskId: UIBackgroundTaskIdentifier = .invalid

bgTaskId = UIApplication.shared.beginBackgroundTask(withName: "ContentView") {
logger.error("background task timed out")
guard bgTaskId != .invalid else { return }
UIApplication.shared.endBackgroundTask(bgTaskId)
bgTaskId = .invalid
}

Task {
await UserManager.user.save()

logger.debug("finished background task")

guard bgTaskId != .invalid else { return }
UIApplication.shared.endBackgroundTask(bgTaskId)
bgTaskId = .invalid
}
}
}
}
}

class UserManager {
static let user = User()
}

struct User {
func save() async {
logger.debug("start save")
try? await Task.sleep(nanoseconds: 3_000_000_000)
logger.debug("finished save")
}
}

You could then

  • install the app on the device
  • quit Xcode
  • fire up macOS Console app
  • select your mobile device
  • make sure Console is logging “info” and “debug” messages (e.g. “Action” » “Include Info Messages” and “Action” » “Include Debug Messages”)
  • I personally add the “Subsystem” and “Category” columns to the log by control-clicking on the column headers in the messages and adding them
  • I always filter for the salient process, subsystem, or category (so I do not get lost in the voluminous logging output) by typing some search criteria in the upper right corner
  • tap “Start streaming"
  • launch your app on your mobile device
  • return to the Home Screen, to trigger the background process

As you can see, the save task runs correctly and all the log messages appear:

Sample Image


A few unrelated observations:

  • My code snippet uses guard early exits, to avoid towers of braces.

  • I consciously avoid starting the background task in an asynchronous process. We want to avoid races between the app suspending and when the background task is created. So I create the background task before launching the asynchronous process.

  • I always make sure that the background task identifier is not .invalid already before trying to end the task. You want to avoid races between the timeout and the graceful termination.

  • It's probably prudent to supply a name for your background task.

None of these four points are terribly critical to the question at hand, but I wanted to simply demonstrate some best practices: The key observation is to (a) test outside of Xcode; and (b) use unified logging system so that you can monitor the progress. There are other approaches to monitor the progress, but this is probably easiest.

How can I convert to Swift async/await from GCD (DispatchQueue)?

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. At they said in WWDC 2021 video Swift concurrency: Update a sample app:

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 dispatch main.async with a call to MainActor's run function. This takes a block of code to run on the MainActor. run is an async function, so we need to await it. Awaiting is necessary because this function may need to suspend until the main thread is ready to process this operation. But because

Is it necessary to always use async in all functions when await is used in Swift

Calling an async function requires a matching await. To make it work in a sync context you need to wrap the call in a task.

Task { await call5() }

This will spawn a concurrent task on the current thread.

Here is the WWDC talk with more details about structured concurrency. https://developer.apple.com/videos/play/wwdc2021/10134/



Related Topics



Leave a reply



Submit