Asynchronously marked functions were introduced with
SE-0296 for Swift 5.5 to make asynchronous calls easier to implement and to read. This allows for functions to opt into being declared and handled as
async, allowing for complex asynchronous operations using the known control flow. Calling such a function can simply be achieved by using the
await or, if the function throws, the
await throws keywords. This addition to the Swift programming language was made to remove the complexity of using completion handlers to handle asynchronous callbacks.
In version 9.1, the SAP BTP SDK for iOS has changed it's own OData frameworks to apply to this improved way of implementing asynchronous functions by providing function definitions within the framework's own API using the SAP BTP SDK for iOS Assistant (Assistant). That means if you are using the SAP BTP SDK for iOS version 9.1 and higher to create a new app you will have the async/await feature out of the box. The team of the SDK has introduced the
SAP BTP SDK for iOS - OfflineDataServiceAsync class which is basically using the Async/Await methodology to introduce this new language feature within the SAP BTP SDK for iOS.
To understand why the introduction of these language changes within Swift 5.5 are such a great improvement let us look at an example.
Important to mention is that if you have an app already in place you can use the Assistant to re-generate the Proxy Classes applying this new feature to your Proxy Classes easily.
Understanding the changes
Before Swift 5.5 the common way of declaring and implementing asynchronous functions was by using completion handlers. Completion Handlers allow us to send back values after a function returns. This sounds great but comes with hard to read syntax in most cases. In the following example we will fetch a product from the backend. For simplicity reasons I have left out the actual networking code and just return hard coded values:
import Foundation
import SAPOData
// Not including error handling here
func fetchProduct(withKey: Int, completion: @escaping (Product) -> Void) {
DispatchQueue.global().async {
let product = executeQuery(service: self, query: query.fromDefault(ESPMContainerMetadata.EntitySets.customers)
completion(product)
}
}
func fetchProductImage(from: Product, completion: @escaping (UIImage) -> Void) {
DispatchQueue.global().async {
let image = UIImage(data: executeQuery(service: self, query: query.fromDefault(product))
completion(image)
}
}
So far, so good, but the code above has multiple issues in itself even if it is syntactically correct:
- The parameter syntax @escaping((Customer?, Error?) -> Void) makes the code harder to read.
- Functions which call their completion handler might call it more than once, or forget to call it at all.
- Calling such a function can end up in a so-called pyramid of doom, code can get increasingly indented for each completion handler.
Calling these functions will end up in indentation:
fetchProduct(222344) { product in
fetchProductImage(product) { image in
displayProductImage(image)
}
}
With Swift 5.5, such functions can be cleaned up by simply marking them as asynchronous, returning a value instead of relying on completion handlers:
import Foundation
import SAPOData
// Not including error handling here
func fetchProduct(withKey: Int) async -> Product {
return executeQuery(service: self, query: query.fromDefault(ESPMContainerMetadata.EntitySets.customers)
}
}
func fetchProductImage(from: Product) async -> UIImage {
return UIImage(data: executeQuery(service: self, query: query.fromDefault(product))
}
}
Calling them now is way simpler now:
func displayProductImage() async {
let product = await fetchProduct(222344)
let productImage = await fetchProductImage(product)
print(productImage)
}
}
Important to notice is that just because we define a function as asynchronous it does not mean it runs concurrently with our other code. That being said, if you don't specify differently calling these functions will still execute sequentially.
SAP BTP SDK for iOS Assistant 9.1 - Async/Await Data Service
I have blown out one of these methods to give you a clearer picture on what the changes involve:
private func fetchCustomer(matching query: DataQuery, headers: HTTPHeaders? = nil, options: RequestOptions? = nil) throws -> Customer {
return try CastRequired<Customer>.from(ProxyInternal.executeQuery(service: self, query: query.fromDefault(ESPMContainerMetadata.EntitySets.customers), headers: headers, options: options).requiredEntity())
}
open func fetchCustomer(matching query: DataQuery, headers: HTTPHeaders? = nil, options: RequestOptions? = nil) async throws -> Customer {
return try await withUnsafeThrowingContinuation {
(continuation: UnsafeContinuation<Customer, Error>) in
asyncFunction {
do {
try self.checkIfCancelled(options?.cancelToken)
let result = try self.fetchCustomer(matching: query, headers: headers, options: options)
continuation.resume(returning: result)
} catch {
continuation.resume(throwing: error)
}
}
}
}
The above shown code sample includes to methods, one private and one which is the actual open API method. The first is basically calling the OData service by executing the fetch query against the Customer entity using a given ID. The second method is implementing the Async/Await approach of doing asynchronous calls in Swift 5.5 and higher. This call is basically saying, "let us execute the private method doing the query execution, wait for the call to complete and resume with the continuation returning the customer or throw the given error. A continuation in Swift helps you to interface asynchronous tasks with synchronous calls. This is because the private method is calling the OData service in a synchronous way but we want to actually execute this within our new Async/Await implementation.
With this Blog Post I simply wanted to introduce the idea
SE-0296 has in mind and emphasize that the Assistant has been changed to fully adapt the Async/Await change, by giving you all the magic out of the box through generating a new app.
To better grasp the new changes and the idea behind what I am explaining here, I would encourage you to download the
latest SAP BTP SDK for iOS and generate yourself a new app to see and experience this new API.
In the Further Reading section you will find additional resources going into more detail on how the Async/Await works.
In the next Blog Post I will introduce you to the approach of changing a legacy app to fully adapt the Async/Await feature.
Until next time - Happy Coding! 🧑
💻👩💻👨💻
Further Reading: