初めに
タイトル通りですが、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
を実装する方法を載せておきます。
- 自前で実装する場合
- ライブラリで実装する場合