「Swiftにしかないasync function」って?
この一文を見たみなさんは
「いやいや、JavaScript(や他言語)にもasync functionはあるよw」
と思われたことでしょう。
ですが
それらの言語の「async function」はSwiftの「async function」とは異なります。
Swiftの「async function」と「async throws function」
Swiftには「async function」と「async throws function」があります。
// throws function
func hogeThrows() throws -> {}
// async function
func hogeAsync() async -> {}
// async throws function
func hogeAsyncThrows() async throws -> {}
Swiftには検査例外があり「失敗する関数=throws function」が存在していました。
そこに、Swift 5.5で「非同期関数=async function」が実装されました。
重要なのは、「async」は「throws」を含有しておらず、それぞれ独立に関数に付与できるという点です。
つまり、Swiftの「async function」は検査例外を出しません。つまり「失敗しない非同期関数」です。
そして、Swiftの「async throws function」がJavaScript等の言語の「async function」に当たり、「失敗する非同期関数」となります。
以上より、Swiftにしかないasync function という意味がわかったかと思います。
async functionの良さと活用方法
「失敗しない非同期関数」は何が良いのでしょうか?
それは、呼び出し時にエラーハンドリングを必要としない点です。
実際に活用されている場所を見るのが一番分かりやすいと思います。
活用例1: refreshable
定義
func refreshable(action: @escaping () async -> Void) -> some View
使用例
List(mailbox.conversations) { conversation in
ConversationCell(conversation)
}
.refreshable {
await mailbox.fetch()
}
これは、SwiftUIでリスト表示のいわゆる「プルリフ」を実装するメソッドです。
このメソッド定義には表示状態を管理する isRefreshing: Binding<Bool>
がなく、リフレッシュ開始時に実行する関数action: @escaping () async -> Void
しかありません。
ではどうやって表示終了を指定するのでしょう?
もうお気づきかもしれませんが、そうです、リフレッシュ開始時に実行する関数が「async function」となっているので、 この関数がawaitしている間、アニメーションが表示され続ける という仕様になっています。
The indicator remains visible for the duration of the refresh, which runs asynchronously:
実用例としては以下のようになります。
@MainActor
final class ViewModel: ObservableObject {
let model: Model
@Published var datas: [SomeData] = []
func onRefresh() async {
do {
datas = try await model.fetchSomeDatas()
} catch {
// エラーハンドリング
}
}
}
// View
List(viewModel.datas) {...}
.refreshable {
await viewModel.onRefresh()
}
Modelの状態ではasync throwsなのを、ViewModelでthrowsを取り除いてasyncにし、失敗しない状態でViewに公開することができます。
この、非同期処理の待機をViewの描画に安全に利用できる、というのがSwiftのasync functionにしかできない強み だと思います。
活用例2: CBCentralManager
過去に書いた記事で、「Bluetoothの許可アラートが表示されている間は待機し、許可された後に処理を実行したい」というケースを紹介しました。これは「失敗しない非同期処理」でした。
活用例: 補足
async/awaitがSwiftに来る前に使っていた人も多いであろう、Swift向けPromiseライブラリの PromiseKit
にも、Guarantee
という「失敗しないPromise」が存在しました。
前から「失敗しない非同期処理」の需要があったということだと思います。
まとめ
Swiftにあった検査例外は元々素晴らしい言語仕様でしたが、async/awaitの登場によってさらに検査例外の素晴らしさが増した共に、他の言語にはない「失敗しない非同期処理」という概念と活用例を生み出しました。
活用例で紹介したrefreshableのような使い方が、今後もっと開拓されていくと面白いなと思います。
余談
Swift以外の言語を使っていて、「実装で多分失敗しないことがわかっている(=例外を投げていない)のに、言語上例外が出る可能性があるから、クラッシュしないようにエラーハンドリングしておくか...」と渋々try-catchを書いた方もいるのではないでしょうか。
Kotlinなんかは、Javaが検査例外があるんだからいい感じにラップして検査例外を実装すればいいのにと思ったりしています。