UIインタラクションを挟むリトライループの形成

例えばユーザが送信ボタンを押したときにサーバへ情報を送り、送信成功画面を表示する、というUIを作りたいとします。
通信エラー等を考慮し、送信エラーの際はアラートを表示し、リトライできるようにしておきたいのですが、このあたりを考慮したビューモデルを以下のように実装してみます。

```swift: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: {}
            )
        }
    }
}
```

対するビューはこんな感じです。

```swift: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を導入してみます。

```swift: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` のような状態変数を監視することなく、以下のように実装できます。

```swift:ContentView.swift
struct ContentView: View {
    ...

    var body: some View {
        VStack {
            ...
            Button("Submit") {
                Task {
                    if await viewModel.submit() {
                        // submit()成功後の処理
                    }
                }
            }
        }
        .alert(...)
    }
}
```

どちらが良いというわけではなく、状況に応じて使い分けてくのがいいかなと思っています。