Swift: Asynchronous Callback

Swift: Asynchronous callback

I've shared the pattern that I use for this scenario in the following gist: https://gist.github.com/szehnder/84b0bd6f45a7f3f99306

Basically, I create a singleton DataProvider.swift that setups an AFNetworking client. Then the View Controllers call methods on that DataProvider, each of which is terminated by a closure that I've defined as a typealias called ServiceResponse. This closure returns either a dictionary or an error.

It allows you to very cleanly (imo) call for an async data action from the VC's with a very clear indication of what you want performed when that async response returns.

DataProvider.swift

typealias ServiceResponse = (NSDictionary?, NSError?) -> Void

class DataProvider: NSObject {

var client:AFHTTPRequestOperationManager?
let LOGIN_URL = "/api/v1/login"

class var sharedInstance:DataProvider {
struct Singleton {
static let instance = DataProvider()
}
return Singleton.instance
}

func setupClientWithBaseURLString(urlString:String) {
client = AFHTTPRequestOperationManager(baseURL: NSURL.URLWithString(urlString))
client!.operationQueue = NSOperationQueue.mainQueue()
client!.responseSerializer = AFJSONResponseSerializer()
client!.requestSerializer = AFJSONRequestSerializer()
}

func loginWithEmailPassword(email:String, password:String, onCompletion: ServiceResponse) -> Void {
self.client!.POST(LOGIN_URL, parameters: ["email":email, "password":password] , success: {(operation:AFHTTPRequestOperation!, responseObject:AnyObject!) -> Void in

self.setupClientWithBaseURLString("http://somebaseurl.com")

let responseDict = responseObject as NSDictionary
// Note: This is where you would serialize the nsdictionary in the responseObject into one of your own model classes (or core data classes)
onCompletion(responseDict, nil)
}, failure: {(operation: AFHTTPRequestOperation!, error:NSError!) -> Void in
onCompletion(nil, error)
})
}
}

MyViewController.swift

import UIKit

class MyViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()

// Do any additional setup after loading the view.
}

override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
DataProvider.sharedInstance.loginWithEmailPassword(email:"some@email.com", password:"somepassword") { (responseObject:NSDictionary?, error:NSError?) in

if (error) {
println("Error logging you in!")
} else {
println("Do something in the view controller in response to successful login!")
}
}
}
}

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()

Swift: how to wrap `completion` into an async/await?

You are looking for continuations.

Here is how to use it in your example:

func start() async {
await withCheckedContinuation { continuation in
start(continuation.resume)
}
}

How to use callbacks to make Asynchronous calls in Swift so that main UI does not Hang?

You are getting confuse between background threads and completion handler.

logInWithUsernameInBackground is a async function that runs in the background however all the code that runs in the completion handler runs back in the main thread:

 completion: ((result:Bool?) -> Void)!) {//The code here runs in the main thread}

So basically from the time that your application start communicate with Parse.com, until the result was back, that code was running asynchronous in a background thread, when it finish and your application received the response it runs in the completion block back in the main thread.

Now lets say you really want to run some code in the background in the completion block, than you would use something like:

let qualityOfServiceClass = QOS_CLASS_BACKGROUND
let backgroundQueue = dispatch_get_global_queue(qualityOfServiceClass, 0)
dispatch_async(backgroundQueue, {
println("Now this code will run in the background thread")
})

All the new quality of service classes are:

QOS_CLASS_USER_INTERACTIVE
QOS_CLASS_USER_INITIATED
QOS_CLASS_UTILITY
QOS_CLASS_BACKGROUND

Or in your case:

PFUser.logInWithUsernameInBackground(uname, password:pwd) {
(user, error) -> Void in
if error == nil {
if user != nil {
// Yes, User Exists

let qualityOfServiceClass = QOS_CLASS_BACKGROUND
let backgroundQueue = dispatch_get_global_queue(qualityOfServiceClass, 0)
dispatch_async(backgroundQueue, {
sleep(10)
})
completion(result: true)

} else {
completion(result: false)
}
} else {

completion(result: false)
}
}

For more information see Apple documentation

Accessing Google API data from within 3 async callbacks and a function in SwiftUI

Solved my own problem.

It appears (according to Apple's async/await intro video) that when you have an unsupported callback that you need to run asynchronously, you wrap it in something called a Continuation, which allows you to manually resume the function on the thread, whether throwing or returning.

So using that code allows you to run the Google Identity token refresh with async/await.

private static func auth(_ user: GIDGoogleUser) async throws -> GIDAuthentication? {
typealias AuthContinuation = CheckedContinuation<GIDAuthentication?, Error>

return try await withCheckedThrowingContinuation { (continuation: AuthContinuation) in
user.authentication.do { authentication, error in
if let error = error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: authentication)
}
}
}
}

static func search(user: GIDGoogleUser) async throws {

// some code

guard let authentication = try await auth(user) else { ... }

// some code
}

I then ran that before using Alamofire's built-in async/await functionality for each request (here's one).

let dataTask = AF.request(...).serializingDecodable(NameResponseModel.self)
let response = try await dataTask.value

return response.values[0]

Push view with data from asynchronous callback

Based on the if let method from @pawello2222's answer, I've created this ViewModifier that let's you push a View to the Navigation Stack. It follows the same pattern as the .sheet modifier.

import SwiftUI

private struct PushPresenter<Item: Identifiable, DestinationView: View>: ViewModifier {

let item: Binding<Item?>
let destination: (Item) -> DestinationView
let presentationBinding: Binding<Bool>

init(item: Binding<Item?>, onDismiss: (() -> Void)? = nil, content: @escaping (Item) -> DestinationView) {
self.item = item
self.destination = content

presentationBinding = Binding<Bool> {
item.wrappedValue != nil
} set: { isActive in
if !isActive && item.wrappedValue != nil {
onDismiss?()
item.wrappedValue = nil
}
}
}

func body(content: Content) -> some View {
ZStack {
content
NavigationLink(
destination: item.wrappedValue == nil ? nil : destination(item.wrappedValue!),
isActive: presentationBinding,
label: EmptyView.init
)
}
}
}

extension View {

/// Pushed a View onto the navigation stack using the given item as a data source for the View's content. **Notice**: Make sure to use `.navigationViewStyle(StackNavigationViewStyle())` on the parent `NavigationView` otherwise using this modifier will cause a memory leak.
/// - Parameters:
/// - item: A binding to an optional source of truth for the view's presentation. When `item` is non-nil, the system passes the `item`’s content to the modifier’s closure. This uses a `NavigationLink` under the hood, so make sure to have a `NavigationView` as a parent in the view hierarchy.
/// - onDismiss: A closure to execute when poping the view of the navigation stack.
/// - content: A closure returning the content of the view.
func push<Item: Identifiable, Content: View>(
item: Binding<Item?>,
onDismiss: (() -> Void)? = nil,
content: @escaping (Item) -> Content) -> some View
{
self.modifier(PushPresenter.init(item: item, onDismiss: onDismiss, content: content))
}
}

And then use it like so:

struct NavigationExampleView: View {

@ObservedObject var vm: NavigationExampleViewModel

var body: some View {
Button("Get User") {
vm.getUser()
}
.push(item: $vm.pushUser, content: UserView.init)
}
}

Be sure to keep your implementing view in a NavigationView for the push to take effect.

Edit

I found an issue with this solution. If using a reference type as the binding item, the object will not get deinitialised on dismiss. It seems like the content is holding a reference to the item. The Object will only the deinitialised once a new object is set on the binding (next time a screen is pushed).

This is also the case for @pawello2222's answer, but not an issue when using the sheet modifier. Any suggestions on how to solve this would be much welcomed.

Edit 2

Adding .navigationViewStyle(StackNavigationViewStyle()) to the parent NavigationView removes the memory leak for some reason. See answer here for more details.

Using the if-let-method caused the push view transition to stop working. Instead pass nil to the NavigationLinks destination when the data is not pressent. I've updated ViewModifier to handle this.

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")
}
}


Related Topics



Leave a reply



Submit