SwiftUI x Concurrency で状態管理から解放されるライブラリを作りました
この記事は Swift/Kotlin愛好会 Advent Calendar 2022の12/11の記事です。
最近ではSwiftUIも実用的になり、Concurrencyの登場によってasync/awaitの使用もできるようになり両者を活用している方もいらっしゃるのではないでしょうか。今回はそんな人にぴったりなライブラリを作りました。そのライブラリの紹介記事になります。
いつも書く処理
モバイルアプリではViewの表示時にAPIからデータを非同期に取得する処理を書くと思います。下記の例ではAPIから情報を取得して、Userの情報を表示するViewになっています。
struct UserView: View {
@State var user: User?
@State var error: Error?
var body: some View {
Group {
if let user {
UserContent(user: user)
} else if let error {
ErrorPage(error: error)
} else {
ProgressView()
}
}
.task {
do {
user = try await apiClient.fetch(path: "user")
} catch {
self.error = error
}
}
}
}
もしくはデータの取得にObservableObjectを用意する方もいるかもしれません。いずれにせよ本質的には
- task(もしくはonAppear)時にAPIを実行してデータの取得
- APIから正常にデータを取得できればメインのコンテンツ、エラーの場合はエラーページ、APIの結果が返ってきてない読み込み時の表示
の2ステップの流れに分解できます。そして、これらの処理はViewに限らず同じようなコードを何度も書くことになると思います。ボイラープレートには下記の2点が確認できます。
- データとエラーの
@State
の宣言 -
.task { do { ... } catch { ... } }
の部分
また、Viewのコンテンツを決定する if let ... else if let ... else
の分岐の連続はenumで表現したい気持ちが湧いて来ると思います。switch文を使用することで強制力が上がることが期待できます。具体的にはたとえば else if let error
の部分の考慮漏れを無くすことができます。
if let user {
UserContent(user: user)
} else if let error {
ErrorPage(error: error)
} else {
ProgressView()
}
これらを解決するライブラリを作りました
Async
Async というライブラリを作りました。みなさんスターください
早速ですがAsyncの利用例を見てみましょう。書き方が2つあって @Async
を使用する場合、AsyncView
を使用する場合に分けて書いていきます。
先ほどのUserViewは以下のように書き換えることができます。
@Async
struct UserView: View {
@Async<User> var async
var body: some View {
switch async(fetch).state {
case .success(let user):
UserContent(user: user)
case .failure(let error):
ErrorPage(error: error)
case .loading:
ProgressView()
}
}
private func fetch() async throws -> User {
try await apiClient.fetch(path: "user")
}
}
@Async
というプロパティラッパーが用意されています。これは callAsFunction
が用意されており、この callAsFunction
には async throws
の関数や、Task
や AsyncStream
等をそのまま渡せるようになっています。さらにこれは自分を返しているので、結果的に async(fetch).state
のような構文が実現できています。
実際に定義されているインタフェースです。
@discardableResult public func callAsFunction(_ task: Task<T, Error>) -> Self
async
に渡された関数は内部で実行されます。最初は loading
, そして関数の評価が始まり、success or failure の結果として返ってきます。同時に state
が変化してそれがView
にも伝わり、 loading
-> success
または、loading
-> failure
へとコンテンツを切り替えることができます。
さらに Async
には var error: Error?
プロパティが存在しており、alert
を出したいといった場合も使用できます。詳しくはExampleのコードも見てください
AsyncView
struct UserView: View {
var body: some View {
AsyncView(fetch, when: (
success: { user in
UserContent(user: user)
},
failure: { error in
ErrorPage(error: error)
},
loading: {
ProgressView()
}
))
}
private func fetch() async throws -> User {
try await apiClient.fetch(path: "user")
}
}
AsyncView
というSwiftUIのViewもライブラリから提供しています。第一引数に処理を渡し、第二引数のwhen(success:failure:loading)には各状態のViewを定義することができます。こちらのメリットとしてはSwiftUIのViewになっているので.padding
や.frame
等のmodifierをそのままメソッドチェインで使用できる点が挙げられます。
まとめ
非同期処理の状態遷移を上手く隠蔽できたライブラリができました。最高のライブラリなのでスターお待ちしていますね
スターください ⭐️
おしまい \(^o^)/
Discussion