This article is adapted from the blog of Bennyhuo

The original address: www.bennyhuo.com/2022/02/16/…


Many iOS apps are still written in Objective-C, so how to call asynchronous functions in Objective-C is a problem.

  • Swift Coroutine gossip (1) : What does a Swift coroutine look like?
  • Gossipy Swift coroutine (2) : Rewrite callbacks as async functions
  • Gossiping Swift coroutine (3) : Calls asynchronous functions in a program
  • Gossip Swift Coroutine (4) : TaskGroups and structured concurrency
  • Chatty Swift coroutine (5) : Task cancellation
  • Gossip Swift coroutine (6) : Actor and attribute isolation
  • Gossipy Swift coroutines (7) : Scheduling of GlobalActors and asynchronous functions
  • TaskLocal (8) : TaskLocal
  • Gossipy Swift coroutine (9) : Asynchronous function calls to other languages

From asynchronous callbacks to asynchronous functions

So far, we have explored in detail most of the syntactic designs in Swift coroutines, the most basic and important of which are asynchronous functions.

Before the emergence of asynchronous functions, we usually add callbacks to functions to achieve asynchronous result return. Take Swift’s network request library Alamofire for example, its DataRequest has such a function:

public func responseData(
  queue: DispatchQueue = .main,
  dataPreprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor,
  emptyResponseCodes: Set<Int> = DataResponseSerializer.defaultEmptyResponseCodes,
  emptyRequestMethods: Set<HTTPMethod> = DataResponseSerializer.defaultEmptyRequestMethods,
  completionHandler: @escaping (AFDataResponse<Data- > >)Void
) -> Self {
    .
}
Copy the code

This function has many arguments, but we only need to care about the last one: completionHandler, which is a closure that accepts a type with an argument of AFDataResponse as the result of the request.

Starting with Swift 5.5, we can wrap it as an asynchronous function, adding support for asynchronous return of results, propagation of exceptions, and cancellation responses:

func responseDataAsync(
  queue: DispatchQueue = .main,
  dataPreprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor,
  emptyResponseCodes: Set<Int> = DataResponseSerializer.defaultEmptyResponseCodes,
  emptyRequestMethods: Set<HTTPMethod> = DataResponseSerializer.defaultEmptyRequestMethods
) async throws -> Data {
    try await withTaskCancellationHandler {
        try await withCheckedThrowingContinuation { continuation in
            responseData(
                queue: queue,
                dataPreprocessor: dataPreprocessor,
                emptyResponseCodes: emptyResponseCodes, emptyRequestMethods: emptyRequestMethods
            ) { response in
                switch response.result {
                case .success(let data): continuation.resume(returning: data)
                case .failure(let error): continuation.resume(throwing: error)
                }
            }
        }
    } onCancel: {
        cancel()
    }
}
Copy the code

Moving from asynchronous callbacks to asynchronous functions is always a wrapped process that is not actually easy. Therefore, we prefer that third-party developers provide versions of asynchronous functions along with asynchronous callbacks so that we can use them on demand.

Asynchronous callback in Objective-C

Automatic conversion of Objective-C callback functions

In previous iOS SDKS, there were over 1,000 Objective-C functions that received callbacks in the form of completionHandler. Such as:

- (void)signData:(NSData *)signData 
withSecureElementPass:(PKSecureElementPass *)secureElementPass 
      completion:(void (^)(NSData *signedData, NSData *signature, NSError *error))completion;
Copy the code

This function is equivalent to Swift’s following function declaration:

func sign(_ signData: Data.using secureElementPass: PKSecureElementPass.completion: @escaping (Data? .Data? .Error?). ->Void)
Copy the code

It would take a lot of time and effort to wrap these functions one by one. Therefore, Swift automatically performs some conversions to objective-C functions that receive similar callbacks and meet certain conditions. Taking the above signData function as an example, it can be automatically converted to:

func sign(_ signData: Data.using secureElementPass: PKSecureElementPass) async throws- > (Data.Data)
Copy the code

Let’s briefly analyze the transformation process.

  1. The completion parameter has been removed. Completion is of type Objective-C block and can be used to handle the return of asynchronous results.
  2. The return value of the transformed asynchronous function (Data, Data), which actually corresponds to completion divisionNSError *Two parameters outside. Note that both signedData and signature are of typeNSData *They’re actually nil, just for type mappings, they should map to SwiftData?Type, or in the case of the converted asynchronous functionDataType, that’s because logically if these twoDataIf you return nil, you should pass the argumentNSError *To cause the asynchronous function to throw an exception. This detail must be paid attention to.
  3. The parameters of the completionNSError *Indicates that exceptions may occur in the result. Therefore, the converted asynchronous function throws an exception and is declared as throws.

So what does this conversion have to be?

  • Both the function itself and the parameter callback return void
  • Callbacks can only be called once
  • Functions are either explicitly modified with swift_async or implicitly derived by parameter names, where support for derivation includes:
    • The function has only one parameter and its tags are WithCompletion, WithCompletionHandler, WithCompletionBlock, WithReplyTo, and WithReply.
    • The function has multiple arguments, and the last one is a callback, and it’s labeled completion, withCompletion, completionHandler, withCompletionHandler, completionBlock, WithCompletionBlock, replyTo, withReplyTo, reply or replyTo.
    • The function takes multiple arguments, and the last argument’s label ends with the label listed in the case of one argument, and the last argument is the callback.

Let’s give another example of the function name:

-(void)getUserAsync:(NSString *)name completion:(void (^)(User *, NSError *))completion;
Copy the code

After the transformation:

func userAsync(_ name: String!). async throws -> User?
Copy the code

For Objective-C functions that begin with GET, get is removed from the function name after the conversion. Other rules are consistent with those mentioned earlier.

With this transformation, many objective-C callbacks from older SDKS can be called as Swift’s asynchronous functions, greatly simplifying our development process.

Call Swift’s asynchronous function in Objective-C

Conversely, if we define Swift’s asynchronous function and want to call it in Objective-C, we can declare it as @objc’s asynchronous function, for example:

@objc class GitHubApiAsync: NSObject {
    @objc static func listFollowers(for userName: String) async throws- > [User] {
        try await AF.request("\(GITHUB_API_ENDPOINT)/users/\(userName)/followers").responseDecodableAsync()
    }
}
Copy the code

The GitHubApiAsync class has the listFollowers function equivalent to:

@interface GitHubApiAsync : NSObject
+ (void)listFollowersFor:(NSString * _Nonnull)userName completionHandler:(void (^ _Nonnull)(NSArray<User *> * _Nullable, NSError * _Nullable))completionHandler;
@end
Copy the code

Call Kotlin’s suspend function

Now that you’ve seen the details of how Swift’s asynchronous functions intercall with Objective-C, let’s look at how Kotlin’s suspend functions support being called by Swift. Of course, this feature is still experimental and could change in the future.

Support for Objective-C callbacks

Kotlin 1.4 introduced Swift support for suspend functions in the form of callbacks, for example:

// kotlin
class Greeting {
    fun greeting(a): String {
        return "Hello, ${Platform().platform}!"
    }

    suspend fun greetingAsync(a): String {
        return "Hello, ${Platform().platform}"}}Copy the code

When compiled, objective-C header files are generated as follows:

__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("Greeting")))
@interface SharedGreeting : SharedBase
...
- (NSString *)greeting __attribute__((swift_name("greeting()")));
- (void)greetingAsyncWithCompletionHandler:(void (^)(NSString * _Nullable, NSError * _Nullable))completionHandler __attribute__((swift_name("greetingAsync(completionHandler:)")));
@end;
Copy the code

The generated class is named SharedGreeting, where Shared is the module name. __attribute__((swift_name(“Greeting”))) causes this Objective-C class to map to Swift’s name, Greeting.

Let’s focus on the greetingAsync function, which maps to the following callback form:

- (void)greetingAsyncWithCompletionHandler:(void (^)(NSString * _Nullable, NSError * _Nullable))completionHandler __attribute__((swift_name("greetingAsync(completionHandler:)")));
Copy the code

Support for Swift asynchronous functions

Kotlin’s support for objective-C callbacks matches the previously discussed condition that callbacks are automatically converted to Swift asynchronous functions, so in theory in Swift 5.5, We can also call Kotlin’s suspend function as if it were Swift’s asynchronous function:

// swift
func greet(a) async throws -> String {
    try await Greeting().greetingAsync()
}
Copy the code

Of course, there are some details. Kotlin 1.5.30 followed this up a bit by adding support for _Nullable_result in the generated Objective-C header file, which allows Kotlin’s suspended function to return nullable types, Can be correctly converted to Swift asynchronous functions that return optional types, for example:

suspend fun greetingAsyncNullable(): String? {
    return "Hello, ${Platform().platform}"
}
Copy the code

Notice that the return type declared in this example is String, right? , the objective-C function is generated as follows:

- (void)greetingAsyncNullableWithCompletionHandler:(void (^)(NSString * _Nullable_result, NSError * _Nullable))completionHandler __attribute__((swift_name("greetingAsyncNullable(completionHandler:)")));
Copy the code

In greetingAsyncNullable, the type of the returned value is mapped to NSString * _Nullable_result. In greetingAsync it maps to NSString * _Nullable. The difference between _Nullable_result and _Nullable is that _Nullable_result can make a Swift asynchronous function return optional type (Kotlin nullable type). The latter returns a non-optional type (corresponding to Kotlin’s non-null type, nonnull Type).

Kotlin suspends the exception propagation of the function

If Kotlin’s suspend function is not declared as @throws, only CancellationException will be converted to NSError and thrown into Swift. All others will exit as a critical error. Therefore, if the program needs to be exposed to Swift calls, We generally recommend adding @throws to Kotlin functions that may have exceptions thrown, for example:

// kotlin
@Throws(Throwable::class)
suspend fun greetingAsync(a): String {
    throw IllegalArgumentException("error from Kotlin")
    return "Hello, ${Platform().platform}"
}
Copy the code

The exception can also be caught directly during Swift calls:

//swift
do {
    print(try await Greeting().greetingAsync())
} catch {
    print(error)
}
Copy the code

The program output is as follows:

Error Domain=KotlinException Code=0 "error from Kotlin" UserInfo={NSLocalizedDescription=error from Kotlin, KotlinException=kotlin.IllegalArgumentException: error from Kotlin, KotlinExceptionOrigin=}
Copy the code

Context zero passing

Although currently Kotlin’s suspend functions can be called as Swift’s asynchronous functions, the Kotlin side has not carefully designed and customized the Swift asynchronous function call scenario. So things like Swift side cancel state (which gets Swift’s Task cancel state in the Kotlin suspend function), scheduler (Swift’s actor and scheduler bound to Task), TaskLocal variable, and Kotlin The state of the scheduler, coroutine context, etc. during the execution of the side suspend function is not implemented.

With this in mind, you should try to simplify the design of functions as much as possible to avoid scenarios that are too complex to understand.

summary

In this paper, we discuss the mutual call between async function and Objective-C in Swift coroutine, which introduces the conditions and details of objective-C callbacks automatically mapping into Swift asynchronous functions. And Kotlin suspend support for Swift asynchronous functions.


About the author

Bennyhuo, Kotlin Evangelist, Google Certified Kotlin Development Expert (Kotlin GDE); Author of An In-depth Understanding of Kotlin coroutines (China Machine Press, 2020.6); Former Senior engineer of Tencent, now working in Ape Tutoring

  • GitHub:github.com/bennyhuo
  • Blog: www.bennyhuo.com
  • Bilibili: Bennyhuo is not a fortune teller
  • Wechat official account: Bennyhuo