初めに
タイトル通りですが、CombineのflatMapで行う処理が、一度でもfailureになると、それ以降は値が流れなくなるという困った挙動があります。
かなり前からSwift Forumsとかでも上がっていますが、現在もなお直っていません。
ということで、色々と対応方法を調べたので掲載しておきます。
前提
実装に先立ち、まずは問題の発生したコード例で紹介します。
1. Combine を使ったネットワークコード例
Combineを用いて、以下のような簡単な API クライアントとレスポンス例を用意しました。
final class Repository: NSObject {
static func fetch() -> AnyPublisher<Response, Error> {
URLSession.shared
.dataTaskPublisher(for: URL(string: "https://xxx/yyy/zzz")!)
.map(\.data)
.decode(type: Response.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
}
}
struct Response: Decodable {}
2. flatMap を使った呼び出しのコード
先のネットワークコードを呼び出す時の例を用意しました。
(例えばボタンを押したときにAPIが呼び出されるようなコード)
var cancellable = Set<AnyCancellable>()
var didTapButton = PassthroughSubject<(), Never>()
func bind() {
didTapButton
.flatMap { // ここで呼び出す
Repository.fetch()
}
.sink(receiveCompletion: { result in
switch result {
case .failure(let error): break
case .finished: break
}
}, receiveValue: { response in
// do something
})
.store(in: &cancellable)
}
// 実行
didTapButton.send(())
3. 実装の問題
このとき、API が失敗して一度でも failure に来ると、、、
以後 didTapButton.send(()) をいくら呼んでも値が流れてこない問題が発生します。
これがタイトルでも述べている Combine の flatMap の問題です。
対応方法
今回はこのコード例を修正する形で対応方法を記載したいと思います。
また、方法を2つ掲載しておきます。
① replaceError で回避する
didTapButton
.flatMap {
Repository.fetch()
.replaceError(with: Response())
}
.sink { response in
// do something
}
.store(in: &cancellable)
replaceError を経由することで、暗黙的に AnyPublisher<Response, Never> になるため、failure に入ってくることは無くなります。ただし、成功でも失敗でも sink に一括で値が入ってくるため、何かしらの判定が必要になっています。
② Result で返す
① の sink では帰ってきた値を判定する方法がなかったので、Result 型にすることでこれを実現します。
didTapButton
.flatMap {
Repository.fetch()
.map { Result.success($0) }
.catch { Just(Result.failure($0)) }
}
.sink { result in
switch result {
case .success(let response): break
case .failure(let error): break
}
}
.store(in: &cancellable)
map で Result 型にして、失敗時は catch でハンドリングしています。こちらも先と同様に、暗黙的に AnyPublisher<Result<Response, Error>, Never> になるため、failure に入ってくることは無くなります。
この書き方は、せっかく Combine で Response と Error でハンドリングできるものを1つに流してしまうため、Combine の良さは失われてしまいます。
ただし、RxSwift のような書き方ができるため、人によっても見やすいかもしれません。さらに RxSwift の materialize のようなものがあれば、さらに分割できたりもしますが、 Combine にはないのは残念です。
終わりに
Apple が直してくれ!!!
コメントになぜそうなるかのやりとりがあり、わかりやすかったので読んでみてください!
余談
Combine で materialize を実装する方法を載せておきます。
- 自前で実装する場合
- ライブラリで実装する場合