🌟

SwiftUI x Concurrency で状態管理から解放されるライブラリを作りました

2022/12/20に公開

この記事は 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を用意する方もいるかもしれません。いずれにせよ本質的には

  1. task(もしくはonAppear)時にAPIを実行してデータの取得
  2. APIから正常にデータを取得できればメインのコンテンツ、エラーの場合はエラーページ、APIの結果が返ってきてない読み込み時の表示

の2ステップの流れに分解できます。そして、これらの処理はViewに限らず同じようなコードを何度も書くことになると思います。ボイラープレートには下記の2点が確認できます。

  1. データとエラーの@Stateの宣言
  2. .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 というライブラリを作りました。みなさんスターください
https://github.com/bannzai/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 の関数や、TaskAsyncStream 等をそのまま渡せるようになっています。さらにこれは自分を返しているので、結果的に 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のコードも見てください

https://github.com/bannzai/Async/blob/main/Example/AsyncExample/ClosurePage.swift#L50

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をそのままメソッドチェインで使用できる点が挙げられます。

まとめ

非同期処理の状態遷移を上手く隠蔽できたライブラリができました。最高のライブラリなのでスターお待ちしていますね

スターください ⭐️
https://github.com/bannzai/Async

おしまい \(^o^)/

Discussion