🌾
[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