0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-01-05

例えばユーザが送信ボタンを押したときにサーバへ情報を送り、送信成功画面を表示する、という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(...)
    }
}

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

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?