Publisher
#Combine
Combine の Publisher について書きます。
Future
概要
最終的に 1 つの値を送出し終了 (正常終了/失敗) する Publisher。RxSwift におけるSingle に役割としては近そう。Promise という type alias が定義されていて、Promise を引数にとった closure で初期化する。
code:swift
init(_ attemptToFulfill: @escaping (@escaping Future<Output, Failure>.Promise) -> Void)
Promise とは、Publisher が 将来 エラーもしくは値を publish する時に実行される (つまり、Publisher が値を publish した時に、publish した値を受け渡されることが約束 (promise) されている) closureであり、
A type that represents a closure to invoke in the future, when an element or error is available.
https://developer.apple.com/documentation/combine/future/promise
下記のようなシンプルな型に対するエイリアスとなっている。Future における、最終的には 正常終了/失敗 を送出する、というのは、Result 型で表現されていることがわかる。
code:swift
typealias Promise = (Result<Output, Failure>) -> Void
注意点としては、Future の処理の実行タイミングは、初めて subscribe されたタイミング ではなく、Futureインスタンスが生成されたタイミング になるという点。実行を初めて subscribe されたタイミングに遅延させたい場合は、Deferred を利用する。
https://developer.apple.com/documentation/combine/future
使い方
completion handler を用いた、API 呼び出し時の典型的な非同期処理の記法を Future を用いて書き換えるサンプルが以下にある。
Using Combine for Your App’s Asynchronous Code
completion handler は、ある関数の引数に受け渡して実行すると、その関数の実行終了時に呼び出される closure。このパターンは下記のように記述できる。
code:swift
func performAsyncAction(completionHandler: @escaping () -> Void) {
DispatchQueue.main.asyncAfter(deadline:.now() + 2) {
completionHandler()
}
}
これを Future を使って書き直すと下記のようになる。promise が completion handler の役割をになっている。
code:swift
func performAsyncActionAsFuture() -> Future <Void, Never> {
return Future() { promise in
DispatchQueue.main.asyncAfter(deadline:.now() + 2) {
promise(Result.success(()))
}
}
}
下記のように Subscriber をチェインさせて実行することが可能。
code:swift
cancellable = performAsyncActionAsFuture()
.sink() { _ in print("Future succeeded.") }
Just
概要
値を送出して終了する Publisher。エラーは送出しないし、nil ではなく常に値を送出する のが特徴。Publisher の operation のチェーンの中で、単一の値の Publisher に変換したい場合や、単純に publish の起点としてとある値を用いたい場合なんかに利用できる。
使い方
例えば、エラーハンドリング内で以下のように利用できる。
code:swift
let remoteDataPublisher = URLSession.shared.dataTaskPublisher(for: myURL!)
// the dataTaskPublisher output combination is (data: Data, response: URLResponse)
.map({ (inputTuple) -> Data in
return inputTuple.data
})
.decode(type: IPInfo.self, decoder: JSONDecoder())
.catch { err in
return Publishers.Just(IPInfo(ip: "8.8.8.8"))
}
.eraseToAnyPublisher()
https://heckj.github.io/swiftui-notes/#patterns-oneshot-error-handling
Deferred
概要
初期化時に別の Publisher を受け渡すことができ、subscribe されるのを待って (= 遅延, defer) からその Publisher に値を publish する Publisher。
使い方
Future は、以下のような特徴がある。
インスタンス生成時に即時実行される
結果は1度しか送出されない
内部の closure も一度しか実行されない
複数回 subscribe しても同じ値しか渡ってこない
そのため、subscribe する度に (したタイミングで) 内部の非同期処理を実行したい 場合には、Deferred で以下のように包んでやると良い。これで、Future 内の closure の実行は subscribe されたタイミングに遅延され、さらに subscribe の度に closure が実行されるようになる。
code:swift
func createFuture() -> AnyPublisher<Int, Never> {
return Deferred {
Future { promise in
print("Closure executed")
promise(.success(42))
}
}.eraseToAnyPublisher()
}
let future = createFuture() // nothing happens yet
let sub1 = future.sink(receiveValue: { value in
print("sub1: \(value)")
}) // the Future executes because it has a subscriber
let sub2 = future.sink(receiveValue: { value in
print("sub2: \(value)")
}) // the Future executes again because it received another subscriber
https://www.donnywals.com/using-promises-and-futures-in-combine/