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 NavigationLink
s 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:
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
Uicollectionview Select and Deselect Issue
Ipad: Drag a Uitableviewcell from One Uitableviewcontroller and Drop It into Another
Objc_Sync_Enter/Objc_Sync_Exit Not Working with Dispatch_Queue_Priority_Low
iOS Playground Doesn't Show UI Preview
How to Select Range of Values When Using Arc4Random()
iOS Provisioning and Certifcates - Will Revoke/Renew Effect App Store Apps
How to List All Classes Conforming to Protocol in Swift
Swiftui Datepicker Jumps Between Short and Medium Date Formats When Changing the Date
Detect When a Webview Video Becomes Fullscreen on iOS8
How to Set Image in Circle in Swift
How Does Google's Custom iOS Keyboard, Gboard, Programmatically Dismiss the Frontmost App
iOS Autolayout Vertically Equal Space to Fill Parent View
Avplayer Resuming After Incoming Call
How to Know Which Is the Default Measure System (Imperial or Metric) on iOS
Uiview. Why Does a Subviews Outside Its Parent's Extent Not Receive Touches