🌾
[Swift] 【Combine入門】AnyPublisher の Output を Never にした場合の挙動
はじめに
Combine で処理を連結するときに、AnyPublisher の Output に空の Void を返して、何も返り値がないことを暗に示していたのですが、それよりは .ignoreOutput() して Output を Never にしたほうが意図が伝わるのでは? と思い、それで実装したところ思ったように動かなかったため、挙動を整理しました。
環境:Xcode 12.5.1
結論
先に結論から述べます。
-
AnyPublisherのOutputがNeverになった段階でストリームは流れなくなる - 何も返り値がない場合の
AnyPublisherのOutputはVoidを返して、Neverにしたい箇所で.ignoreOutput()とつけてNeverにしてあげるのが良さそう
実験その1
CurrentValueSubject<Int, Never> の number が 0 か負の値かチェックするという適当な処理を例にして実験します。
import Combine
import Foundation
enum TestError: Error {
case zeroError
case minusError
}
var cancellables: Set<AnyCancellable> = []
let number = CurrentValueSubject<Int, Never>(0)
func start() {
number
.dropFirst()
.setFailureType(to: TestError.self)
.flatMap { _ -> AnyPublisher<Never, TestError> in
print("checkZero")
return checkZero()
}
.flatMap { _ -> AnyPublisher<Never, TestError> in
print("checkMinus") // 上のflatMapの段階でNeverになるのでここにストリームは流れてこない
return checkMinus()
}
.sink { completion in
switch completion {
case .finished:
print("finished")
case let .failure(error):
print("error: \(error)")
}
print("completion.")
} receiveValue: { hoge in
print("receiveValue: \(hoge)")
}
.store(in: &cancellables)
}
func checkZero() -> AnyPublisher<Never, TestError> {
Future<Void, TestError> { promise in
let numberValue = number.value
print("now: \(numberValue)")
guard number.value != 0 else {
promise(.failure(TestError.zeroError))
return
}
promise(.success(()))
}
.ignoreOutput()
.eraseToAnyPublisher()
}
func checkMinus() -> AnyPublisher<Never, TestError> {
Future<Void, TestError> { promise in
let numberValue = number.value
print("now: \(numberValue)")
guard numberValue > 0 else {
promise(.failure(TestError.minusError))
return
}
promise(.success(()))
}
.ignoreOutput()
.eraseToAnyPublisher()
}
start()
number.value = 1
number.value = -1 // 本当はここでminusErrorになってほしいがcheckMinusに到達しない
number.value = 0
// 出力
// checkZero
// now: 1
// checkZero
// now: -1
// checkZero
// now: 0
// error: zeroError
// completion.
このように checkMinus()まで処理が到達していないことがわかります。
つまり、前段の checkZero() で止まっていることがわかります。
- ポイント
-
AnyPublisher返す関数側でNeverを返すようにしている -
AnyPublisherのOutputがNeverになった段階でストリームは流れなくなる(本当は流れてほしい)- → 途中の
flatMapでNeverにするとストリームはそこで止まる
- → 途中の
-
実験その2
import Combine
import Foundation
enum TestError: Error {
case zeroError
case minusError
}
var cancellables: Set<AnyCancellable> = []
let number = CurrentValueSubject<Int, Never>(0)
func start() {
number
.dropFirst()
.setFailureType(to: TestError.self)
.flatMap { _ -> AnyPublisher<Void, TestError> in
print("checkZero")
return checkZero()
}
.flatMap { _ -> AnyPublisher<Never, TestError> in // 最後のflatMapでNeverにする
print("checkMinus")
return checkMinus() // AnyPublisher<Void, TestError>
.ignoreOutput() // Void を Never に変換
.eraseToAnyPublisher() // 型合わせ
}
.sink { completion in
switch completion {
case .finished:
print("finished")
case let .failure(error):
print("error: \(error)")
}
print("completion.")
} receiveValue: { hoge in
print("receiveValue: \(hoge)")
}
.store(in: &cancellables)
}
func checkZero() -> AnyPublisher<Void, TestError> { // Voidに変更
Future<Void, TestError> { promise in
let numberValue = number.value
print("now: \(numberValue)")
guard number.value != 0 else {
promise(.failure(TestError.zeroError))
return
}
promise(.success(()))
}
// .ignoreOutput()
.eraseToAnyPublisher()
}
func checkMinus() -> AnyPublisher<Void, TestError> { // Voidに変更
Future<Void, TestError> { promise in
let numberValue = number.value
print("now: \(numberValue)")
guard numberValue > 0 else {
promise(.failure(TestError.minusError))
return
}
promise(.success(()))
}
// .ignoreOutput()
.eraseToAnyPublisher()
}
start()
number.value = 1
number.value = -1
number.value = 0
// 出力
// checkZero
// now: 1
// checkMinus
// now: 1
// checkZero
// now: -1
// checkMinus
// now: -1
// error: minusError
// completion.
// checkZero
// now: 0
今回は checkZero() の flatMap では、Never としていないため、ストリームが流れて、checkMinus() まで処理が流れていることがわかります。
また、最後の AnyPublisher 返す関数側で Never であれば、sink() の receiveValue までストリームがながれていないこともわかります(もしながれていれば、"receiveValue: \(hoge)" が出力されるはず)。
これは、 Never になった段階でストリームは流れなくなる証拠でもあります。
- ポイント
-
AnyPublisher返す関数側でNeverとはせずに、flatMapの中で必要に応じて、Neverに変換する - これなら、ストリーム側でコントールが可能になる
- → 何も返り値がない場合の
AnyPublisherのOutputはVoidを返して、Neverにしたい箇所で.ignoreOutput()とつけてあげるのが良さそう
- → 何も返り値がない場合の
-
まとめ
-
AnyPublisherのOutputがNeverになった段階でストリームは流れなくなる - 何も返り値がない場合の
AnyPublisherのOutputはVoidを返して、Neverにしたい箇所で.ignoreOutput()とつけてNeverにしてあげるのが良さそう
そもそも、こういう処理は async/await でやってあげるとよさそうであるが、 iOS15 以上対応まででよいアプリ開発がいつになったらできるのだろうか、、、
Discussion