例えばユーザが送信ボタンを押したときにサーバへ情報を送り、送信成功画面を表示する、というUIを作りたいとします。
通信エラー等を考慮し、送信エラーの際はアラートを表示し、リトライできるようにしておきたいのですが、このあたりを考慮したビューモデルを以下のように実装してみます。
ContentViewModel.swift
@Observable
final class ContentViewModel {
private(set) var isSuccess = false
// エラー表示用モデル
struct IssueAlert {
let title: String
let message: String
var isPresented: Bool
let retryHandler: () async -> Void
let cancelHandler: () async -> Void
static let empty = ...
}
var issueAlert: IssueAlert = .empty
func submit() async {
do {
try await task() // サーバ通信を行うタスク
isSuccess = true
issueAlert = .empty
} catch {
guard !Task.isCancelled else { return }
issueAlert = .init(
title: "送信エラー",
message: "エラーが発生しました",
isPresented: true,
retryHandler: { await self.submit() },
cancelHandler: {}
)
}
}
}
対するビューはこんな感じです。
ContentView.swift
struct ContentView: View {
@Bindable var viewModel = ContentViewModel()
var body: some View {
VStack {
...
Button("Submit") {
Task { await viewModel.submit() }
}
}
.onChange(of: viewModel.isSuccess) { _, isSuccess in
if isSuccess {
// submit()成功後の処理
}
}
.alert(
viewModel.issueAlert.title,
isPresented: $viewModel.issueAlert.isPresented,
actions: {
Button("リトライ") {
Task { await viewModel.issueAlert.retryHandler() }
}
Button("キャンセル") {
Task { await viewModel.issueAlert.cancelHandler() }
}
},
message: { Text(viewModel.issueAlert.message) }
)
}
}
ただ時々、 isSuccess
などの状態変数の監視ではなく、 await viewModel.submit()
の後に何かしらの処理を書きたくなることがあります。上記のやり方では通信エラー等が発生してもメソッドからreturnしてしまうため、成否に関わらず後続の処理が実行されてしまいます。
成功するまで await viewModel.submit()
の中で留まるようにするにはメソッド内で単純にループするだけでなく、エラー表示も考慮する必要があります。これを実現するためにContinuationを導入してみます。
ContentViewModel.swift
@Observable
final class ContentViewModel {
...
// 最終的な成功または失敗(キャンセル)返す
func submit() async -> Bool {
while true {
do {
try await task()
issueAlert = .empty
break
} catch {
guard !Task.isCancelled else { return false }
// Continuationでユーザアクションを待つ
let retry = await withCheckedContinuation { continuation in
issueAlert = .init(
title: "送信エラー",
message: "エラーが発生しました",
isPresented: true,
retryHandler: { continuation.resume(returning: true) },
cancelHandler: { continuation.resume(returning: false) }
)
}
if !retry {
return false
}
}
}
return true
}
}
これによってビューは isSuccess
のような状態変数を監視することなく、以下のように実装できます。
ContentView.swift
struct ContentView: View {
...
var body: some View {
VStack {
...
Button("Submit") {
Task {
if await viewModel.submit() {
// submit()成功後の処理
}
}
}
}
.alert(...)
}
}
どちらが良いというわけではなく、状況に応じて使い分けてくのがいいかなと思っています。