概要
ScreenCaptureKit 関連のクラスのドキュメントを眺めていたら Discussion に Concurrency Note という項目があることに気付き、特定の形式を持った Objective-C のメソッドが自動的に Swift の非同期関数に変換されることを知りました。
Calling Objective-C APIs Asynchronously に詳細が書かれています。簡単なサンプルプロジェクトも作成して挙動を見てみました。
環境
- Swift 5.5.2
メソッド変換の条件
Calling Objective-C APIs Asynchronously によると、変換されるメソッドとその Completion Handler は以下を満たす必要があります。
- メソッドの返り値は void である。
- Completion Block の返り値は void である。
- Completion Block は全ての可能な制御フローにおいてただ一度だけ呼ばれる。
また、引数の個数によって変換されるメソッド名の規則が変わります。
引数が一つだけ、つまり引数が Completion Block のみである場合、メソッド名の末尾が
- WithCompletion
- WithCompletionHandler
- WithCompletionBlock
- WithReplyTo
- WithReply
のいずれかであるものが Swift にインポートされる。Completion Block の他に引数がある場合は、メソッド名の末尾が
- completion
- withCompletion
- completionHandler
- withCompletionHandler
- completionBlock
- withCompletionBlock
- replyTo
- withReplyTo
- reply
- replyTo
のいずれかであるものが Swift にインポートされる、という記載がありました。
挙動
Objctive-C で
- (void)handleVoidWithCompletion:(void (^)(void))completion;
のように定義されたメソッドは、Swift で
await objc.handleVoid()
のように呼び出すことができます。Objective-C 側の実装が
- (void)handleVoidWithCompletion:(void (^)(void))completion {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(3);
completion();
});
}
となっているとして、
// ここまでメインスレッドで実行されている (Thread.current.isMainThread が true)
await objc.handleVoid()
// ここもメインスレッドで実行される (Thread.current.isMainThread が true)
のような挙動になります。
注意点
メソッド名の末尾は指定されたものでなくても呼び出しはできる
例えば
- (void)handleVoidWithCompletionHoge:(void (^)(void))completion;
は Swift 側で
await objc.handleVoidWithCompletionHoge()
と末尾が残ったままになるものの、呼び出しはできました。
Objctive-C 側の定義によっては Ambiguous use of ... になる
Objective-C 側で
- (void)handleVoidWithCompletion:(void (^)(void))completion;
- (void)handleVoidWithCompletionHandler:(void (^)(void))completion;
のような2つのメソッド定義がある場合、Swift 側ではいずれも await objc.handleVoid()
での呼び出しになるので、どちらを呼び出していいか判断が付かずに Ambiguous use of 'handleVoid()'
となりエラーになってしまいます。
Completion Block を複数回実行するとクラッシュする
Objective-C 側の実装を
- (void)handleVoidWithCompletion:(void (^)(void))completion {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(3);
completion();
completion();
});
}
のようにしても、
await objc.handleVoid()
と Swift 側で呼び出すことはできます。しかし、実行時に EXC_BAD_ACCESS
が発生してしまいました。ただ一度だけ実行する、という保証は Objective-C 側で担保する必要があるようです。
当然ですが、逆に Completion Block を一度も呼ばないと await から先に進まなくなってしまいます。
まとめ
Objective-C から Swift の非同期関数への変換はよしなにやってくれるようですが、安全性は Objective-C 側の実装で担保する必要があるようです。今さら Objective-C のプログラムを書く機会はなかなかないですが、標準のフレームワークのメソッドを自前で continuation などで async func にする手間が省ける場合もあると考えるとなかなか便利機能かも知れません。