CatHand Blog

アプリ開発やMac弄り

SwiftUIで非同期でViewを更新する

SwiftUIでWebAPI等を呼んでViewを更新するやり方です。

単に更新するだけではなく、エラーだったらAlertを表示したり、読み込み中はインジゲータを表示するようにしたいと思います。

何か非同期処理をする ObservableObject を作ります。

class APILoader<T: Decodable>: ObservableObject {
    
    let api: API
    @Published var result: Result<T, Error>? = nil
    
    init(api: API) {
        self.api = api
        load()
    }
    
    func load() {
        result = nil
        api.get { (result: Result<T, Error>) in
            self.result = result
        }
    }
}

非同期処理が完了すると result が更新されるようにしておいて、それを @Published にしておきます。

View は↓のようなかんじになります。

struct LoadableView<T: Decodable, V: View>: View {
    
    @State var showingAlert = true

    @ObservedObject var loader: APILoader<T>
    let content: (T) -> V

    var body: some View {
        guard let result = loader.result else {
            return AnyView(IndicatorView())
        }
        switch result {
        case .success(let result):
            return AnyView(content(result))
        case .failure(let error):
            return AnyView(
                Text("再読込")
                    .alert(isPresented: $showingAlert) {
                        Alert(title: Text(""), message: Text(error.localizedDescription))
                    }
                    .padding(20)
                    .onTapGesture(count: 1) {
                        self.loader.load()
                    }
            )
        }
    }
}
  • loader.resultnil
    • ロード中: IndicatorView を表示
  • loader.result が success
    • 成功: content() に結果を渡して表示
  • loader.result が failure
    • 失敗: "再読込"を表示してエラー内容をアラートで表示

というような挙動です。

使う時は

LoadableView(loader: APILoader(api: .home)) { HomeView(home: $0) })

のような形で、APILoader と View を渡すことで汎用的に使うことができます。

contentクロージャを渡して拡張していくのが SwiftUI ぽいのかなと思っています。