【Swift/Combine】retryメソッドの使い方!特定のエラー発生で再度実行する方法

この記事からわかること
- SwiftのCombineフレームワーク
- retryメソッドの使い方
- エラー発生の場合に再試行する方法
- 特定のエラー発生の場合のみ再試行させるには?
index
[open]
\ アプリをリリースしました /
環境
- Xcode:15.0.1
- iOS:17.1
- Swift:5.9
- macOS:Sonoma 14.1
retryメソッドの使い方
func retry(_ retries: Int) -> Publishers.Retry<Self>
SwiftのCombineフレームワークのretry
はエラーが発生した際再度パブリッシャーを実行(再試行)するためのメソッドです。引数には再試行を行いたい回数をInt
型で渡します。
let publisher = PassthroughSubject<Int, MyError>()
enum MyError: Error {
case someError1
case someError2
}
let subscription = publisher
.catch { error -> AnyPublisher<Int, MyError> in
print("エラー発生")
return Fail<Int, MyError>(error: error).eraseToAnyPublisher()
}
.retry(1)
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
print("完了")
case .failure(let error):
print("Error:", error)
}
}, receiveValue: { value in
print("Value:", value)
})
publisher.send(completion: .failure(.someError1))
エラーの発生が分かりやすいようにcatch
メソッドを使用して検知する度に出力してみると以下のようになります。send
メソッドでエラーを送っているのは1回ですが2回「エラー発生」が出力されていることがわかります。
おすすめ記事:【Swift/Combine】catchメソッドの使い方!エラーを補足する方法
エラー発生
エラー発生
Error: someError1
エラーが発生した場合その位置で一旦終了してpublisherの再試行が走るのでretry
をcatch
の上に実装した場合は「エラー発生」は1回しか出力されません。
let subscription = publisher
.retry(1)
.catch { error -> AnyPublisher<Int, MyError> in
print("エラー発生")
return Fail<Int, MyError>(error: error).eraseToAnyPublisher()
}
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
print("完了")
case .failure(let error):
print("Error:", error)
}
}, receiveValue: { value in
print("Value:", value)
})
エラー発生
Error: someError1
特定のエラーが発生した場合のみ再試行する
Combineに定義されているretry
メソッドは先ほどのみで引数に渡せるのは再試行回数だけとなっています。ですが「特定のエラーが発生した場合のみ再試行」といった要件を満たしたい場合は以下の記事で紹介されている、カスタムでオペレーターを実装することで対応することができるようです。
参考文献:Swift combine retry only for some error types
extension Publishers {
struct RetryIf<P: Publisher>: Publisher {
typealias Output = P.Output
typealias Failure = P.Failure
let publisher: P
let times: Int
let condition: (P.Failure) -> Bool
func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
guard times > 0 else { return publisher.receive(subscriber: subscriber) }
publisher.catch { (error: P.Failure) -> AnyPublisher<Output, Failure> in
if condition(error) {
return RetryIf(publisher: publisher, times: times - 1, condition: condition).eraseToAnyPublisher()
} else {
return Fail(error: error).eraseToAnyPublisher()
}
}.receive(subscriber: subscriber)
}
}
}
extension Publisher {
func retry(times: Int, if condition: @escaping (Failure) -> Bool) -> Publishers.RetryIf<Self> {
Publishers.RetryIf(publisher: self, times: times, condition: condition)
}
}
使用する場合は以下のように使用できるようです。
let subscription = publisher
.catch { error -> AnyPublisher<Int, MyError> in
print("エラー発生")
return Fail<Int, MyError>(error: error).eraseToAnyPublisher()
}
.retry(times: 4) { error in
switch error {
case MyError.someError1:
return true
default:
return false
}
}
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
print("完了")
case .failure(let error):
print("Error:", error)
}
}, receiveValue: { value in
print("Value:", value)
})
その他のエラーハンドリングメソッド
オペレーター | 説明 | 使用場面 |
---|---|---|
replaceError | エラーを無視してデフォルト値を流す | エラーを無視して安全に値を流したい場合 |
catch | エラーが発生した際に代わりのPublisherを流す | エラーごとに異なる処理をしたい場合 |
mapError | エラーを別のエラー型に変換する | エラー型を統一したい場合 |
retry | エラーが発生した場合に処理を再試行する | 再試行可能な操作を扱いたい場合 |
tryMap | マッピング中にエラーを投げられる処理を実行する | エラー処理をマッピングと統合したい場合 |
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。