Combineでエラーを扱う方法まとめ
Combineでエラーを扱うことを軸にまとめました。
以下のコードがある前提で話を進めます。
import Foundation
import Combine
var cancellables = Set<AnyCancellable>()
// 1を足す関数のError
enum AddOneError: Error {
case error
}
// 1を足すPublisherを返す関数
func addOnePublisher(_ n: Int) -> AnyPublisher<Int, AddOneError> {
Deferred {
Future { (promise) in
guard n < 10 else {
promise(.failure(AddOneError.error))
return
}
promise(.success(n + 1))
}
}
.eraseToAnyPublisher()
}
sink
エラーを受け取る
エラー専用の機能ではないCombineのいつものあれ。
エラーの場合はcompletionでエラーを受け取ります。
エラーを受け取り何をするかはご自由に。
// sinkでエラーを処理する
addOnePublisher(10)
.sink { (completion) in
switch completion {
case .finished:
break
case .failure(let error): // AnyPublisher<Int, AddOneError>なのでAddOneError型になる
print(error)
}
} receiveValue: { (n) in
print(n)
}
.store(in: &cancellables)
replaceError
エラーを正常な値に置き換える
エラーが発生した場合、そのエラーはなかったこととし正常な値(Publisher.Output)に置き換えます。
似たような機能に、nilを置き換えるreplaceNilや、空を置き換えるreplaceEmptyがあります。
// Error -> Output
addOnePublisher(10)
.replaceError(with: 1) // エラーが発生したらOutput型の1に置き換える
.sink { (completion) in
switch completion {
case .finished:
break
case .failure(let error):
print(error)
}
} receiveValue: { (n) in
print(n)
}
.store(in: &cancellables)
mapError
エラーを別のエラーに変える
エラーを別のエラー型に変換するmap。
Error以外のものには変換できません。
// Error -> Error
enum SomeError: Error {
case error(Error)
}
addOnePublisher(10)
.mapError { error in // AddOneError を受け取る
SomeError.error(error) // SomeError に変換する
}
.sink { (completion) in
switch completion {
case .finished:
break
case .failure(let error):
print(error)
}
} receiveValue: { (n) in
print(n)
}
.store(in: &cancellables)
catch
エラーをPublisherに置き換える
エラーが発生した場合、Publisherに置き換えます。
Publisherに置き換えるので 割となんでもあり です。
// Error -> Publisher(正常な値)
addOnePublisher(10)
.catch { error in
// Output型の1を包んだPublisherに置き換える
Just(1)
}
.sink { (completion) in
switch completion {
case .finished:
break
case .failure(let error):
print(error)
}
} receiveValue: { (n) in
print(n)
}
.store(in: &cancellables)
// Error -> Publisher(別のエラー)
addOnePublisher(10)
.catch { error in
// 別のErrorを包んだPublisherに置き換える
Fail(outputType: Int.self, failure: SomeError.error(error))
}
.sink { (completion) in
switch completion {
case .finished:
break
case .failure(let error):
print(error)
}
} receiveValue: { (n) in
print(n)
}
.store(in: &cancellables)
// Error -> Publisher(空)
addOnePublisher(10)
.catch { error in
// 空のPublisherに置き換え何もなかったことにする
Empty(outputType: Int.self, failureType: Error.self)
}
.sink { (completion) in
switch completion {
case .finished:
break
case .failure(let error):
print(error)
}
} receiveValue: { (n) in
print(n)
}
.store(in: &cancellables)
handleEvents
エラーから副作用
なんでも確認できるhandleEvents。
もちろんErrorも確認できます。
処理が終わった時のエラー処理(sink)とは区別してエラーを扱いたいとき用でしょうか。
// Error なら 副作用(何かする)
addOnePublisher(10)
.handleEvents(receiveCompletion: { (completion) in
if case .failure(let error) = completion {
// エラーの場合に何かする
print(error)
}
})
.sink { (completion) in
switch completion {
case .finished:
break
case .failure(let error):
print(error)
}
} receiveValue: { (n) in
print(n)
}
.store(in: &cancellables)
retry
再実行する
エラーなら再実行します。
.retry(3)
であれば最大3回リトライします。
つまり最初の1回と合わせて最大4回実行されます。
それでもエラーになった場合はエラーとして続きの処理に移ります。
// Error ならリトライ
addOnePublisher(10)
.handleEvents(receiveCompletion: { (completion) in
print(completion) // リトライするたびにエラーが流れてくることがわかる
})
.retry(3) // 3回再実行する
.sink { (completion) in
switch completion {
case .finished:
break
case .failure(let error):
print(error) // 4回実行してもダメならここにくる
}
} receiveValue: { (n) in
print(n)
}
.store(in: &cancellables)
assertNoFailure
エラーなら処理を止める
エラーなら処理を止めます。
debug/realeaseビルドのどちらでも処理が止まるようです。
// Error なら assert
addOnePublisher(10)
.assertNoFailure() // ここで処理が止まる
.sink { (completion) in
switch completion {
case .finished:
break
case .failure(let error):
print(error)
}
} receiveValue: { (n) in
print(n)
}
.store(in: &cancellables)
breakpointOnError
エラーならデバッガーで処理を停止する
エラーならデバッガーで処理を停止します。
実際に試したところ、処理は一時停止したけどどのbreakpointOnError()で止まったのかよくわからず使いにくかったです。使い方が悪いのかな。
// Error なら breakpoint
addOnePublisher(10)
.breakpointOnError()
.sink { (completion) in
switch completion {
case .finished:
break
case .failure(let error):
print(error)
}
} receiveValue: { (n) in
print(n)
}
.store(in: &cancellables)
breakpointを使うと、それ以外の時も止めたり、特定のエラーのみ止めたりできます。
// Error なら breakpoint
addOnePublisher(10)
// 以下の例の場合、breakpointOnErrorと同等になる
.breakpoint(receiveSubscription: nil, receiveOutput: nil, receiveCompletion: { (completion) -> Bool in
if case .failure(_) = completion {
return true
} else {
return false
}
})
.sink { (completion) in
switch completion {
case .finished:
break
case .failure(let error):
print(error)
}
} receiveValue: { (n) in
print(n)
}
.store(in: &cancellables)
setFailureType(to:)
エラーが起きるかもしれないことにする
Failure = Neverの場合絶対にエラーは起きません。しかし続きの処理ではエラーが起きるかもしれない場合に、Failureの型を揃えるために使用します。
iOS14/macOS11ではこの変換は自動で行われるようで、iOS13/macOS10.15をサポートするとき必要になります。
// Never なら Error が起きるかもしれないものとする
// この型変換はiOS14/macOS11なら自動で行われる
Timer.publish(every: 1, on: .main, in: .common) // Neverなのでエラーは発生しない
.autoconnect()
.setFailureType(to: AddOneError.self) // AddOneErrorが発生する可能性があるものとする。
.flatMap { _ in
addOnePublisher(10) // AddOneErrorが発生する可能性がある
}
.sink { (completion) in
switch completion {
case .finished:
break
case .failure(let error):
print(error)
}
} receiveValue: { (n) in
print(n)
}
.store(in: &cancellables)
エラーを発生させる
map
に対するtryMap
など一部オペレータにはtryバージョンが存在します。
tryバージョンではクロージャー内でthrow Errorでき、エラーを発生させることができます。
FailureはError型となります。
// 何か -> Error
addOnePublisher(1)
.tryMap { _ in
throw AddOneError.error // Error型のエラーとして処理される
}
.sink { (completion) in
switch completion {
case .finished:
break
case .failure(let error):
print(error)
}
} receiveValue: { (n) in
print(n)
}
.store(in: &cancellables)
addOnePublisherで使用しているFuture
もエラーを発生させることができます。
Future<Int, AddOneError> { (promise) in
promise(.failure(AddOneError.error)) // AddOneError型のエラーとして処理される
}
.sink { (completion) in
switch completion {
case .finished:
break
case .failure(let error):
print(error)
}
} receiveValue: { (n) in
print(n)
}
.store(in: &cancellables)
flatMap
のようにPublisherを返す場合、tryバージョンはありませんがFailを使ってエラーを発生させることができます。
addOnePublisher(1)
.flatMap { _ in
Fail(outputType: Int.self, failure: AddOneError.error)
}
.sink { (completion) in
switch completion {
case .finished:
break
case .failure(let error):
print(error)
}
} receiveValue: { (n) in
print(n)
}
.store(in: &cancellables)
すぐ上でも使用しているFail
もエラーを発生させることができます。エラーそのものと考えた方がいいでしょうか。
Fail(outputType: Int.self, failure: AddOneError.error)
.sink { (completion) in
switch completion {
case .finished:
break
case .failure(let error):
print(error)
}
} receiveValue: { (n) in
print(n)
}
.store(in: &cancellables)
Discussion