Combine
import Combine
print("---- Just")
Just(1).sink { value in
print(value)
}
print("---- debug print")
Just(1).print("debug").sink { print($0) }
print("---- map")
func twice(_ value: Int) -> Int { return value * 2 }
Just(2).map{ twice($0) }.sink { print($0) }
print("---- typeof: Int")
Just(1).sink { print("typeof: \(type(of: $0))") }
print("---- Transform to string")
Just(1).map { String($0) }.sink { print("typeof: \(type(of: $0))") }
print("---- Future")
let f = Future<String, Never> { promise in
print("in Future")
promise(.success("Future success"))
}.eraseToAnyPublisher()
print("subscribe f")
f.sink { print ("Future value: \($0)") }
print("---- Defered")
let d = Deferred {
return Future<String, Never> { promise in
print("in Deferred")
promise(.success("Future success"))
}
}.eraseToAnyPublisher()
print("subscribe d")
d.sink { print ("Deferred value: \($0)") }
print("---- Publisher from sequence")
let integers = (0...3)
integers.publisher.sink { print("Received \($0)") }
print("---- handleEvents")
(0...3).publisher
.handleEvents(receiveSubscription: { subs in
print("Subscription: \(subs.combineIdentifier)")
}, receiveOutput: { anInt in
print("in output handler, received \(anInt)")
}, receiveCompletion: { _ in
print("in completion handler")
}, receiveCancel: {
print("received cancel")
}, receiveRequest: { (demand) in
print("received demand: \(demand.description)")
})
.sink { _ in return }
---- Just
1
---- debug print
debug: receive subscription: (Just)
debug: request unlimited
debug: receive value: (1)
1
debug: receive finished
---- map
4
---- typeof: Int
typeof: Int
---- Transform to string
typeof: String
---- Future
in Future
subscribe f
Future value: Future success
---- Defered
subscribe d
in Deferred
Deferred value: Future success
---- Publisher from sequence
Received 0
Received 1
Received 2
Received 3
---- handleEvents
Subscription: 0x7fcf624a1840
received demand: unlimited
in output handler, received 0
in output handler, received 1
in output handler, received 2
in output handler, received 3
in completion handler
import Combine
print("---- PassthroughSubject")
let subject = PassthroughSubject<Int, Never>()
var cancellable = Set<AnyCancellable>()
subject.handleEvents(receiveSubscription: { subs in
print("Subscription: \(subs.combineIdentifier)")
}, receiveOutput: { anInt in
print("in output handler, received \(anInt)")
}, receiveCompletion: { _ in
print("in completion handler")
}, receiveCancel: {
print("received cancel")
}, receiveRequest: { (demand) in
print("received demand: \(demand.description)")
}).sink{ print($0) }.store(in: &cancellable)
//cancellable.removeAll()
subject.send(1)
subject.send(completion: .finished)
---- PassthroughSubject
Subscription: 0x7fca9bd98e80
received demand: unlimited
in output handler, received 1
1
in completion handler
import Combine
print("---- PassthroughSubject")
let subject = PassthroughSubject<Int, Never>()
var cancellable = subject.handleEvents(receiveSubscription: { subs in
print("Subscription: \(subs.combineIdentifier)")
}, receiveOutput: { anInt in
print("in output handler, received \(anInt)")
}, receiveCompletion: { _ in
print("in completion handler")
}, receiveCancel: {
print("received cancel")
}, receiveRequest: { (demand) in
print("received demand: \(demand.description)")
}).sink{ print($0) }
subject.send(1)
subject.send(2)
cancellable.cancel()
subject.send(3)
---- PassthroughSubject
Subscription: 0x7fd2d80545f0
received demand: unlimited
in output handler, received 1
1
in output handler, received 2
2
received cancel
import Combine
let subject = CurrentValueSubject<[Int], Never>([])
var cancellable = subject.dropFirst().sink{ print($0) }
subject.value.append(1)
subject.value.append(2)
subject.value.append(3)
subject.send(completion: .finished)
// dropFirst()を外すと初期値が表示される
/* Prints:
[1]
[1, 2]
[1, 2, 3]
*/
import Combine
let subject = PassthroughSubject<Int, Never>()
var cancellable = subject.sink{ print($0) }
subject.send(1)
subject.send(completion: .finished)
/* Prints:
1
*/
eraseToAnyPublisher()
とAnyPublisher
について
eraseToAnyPublisherの公式リファレンスより
原文
Use eraseToAnyPublisher() to expose an instance of AnyPublisher to the downstream subscriber, rather than this publisher’s actual type. This form of type erasure preserves abstraction across API boundaries, such as different modules. When you expose your publishers as the AnyPublisher type, you can change the underlying implementation over time without affecting existing clients.
DeepL
下流の購読者に AnyPublisher のインスタンスを公開するには、このパブリッシャーの実際の型ではなく、 eraseToAnyPublisher() を使用します。この形式の型の消去は、異なるモジュールなどの API の境界を越えて抽象化を維持します。パブリッシャーを AnyPublisher タイプとして公開すると、既存のクライアントに影響を与えることなく、基本的な実装を変更することができます。
原文
The following example shows two types that each have a publisher property. TypeWithSubject exposes this property as its actual type, PassthroughSubject, while TypeWithErasedSubject uses eraseToAnyPublisher() to expose it as an AnyPublisher. As seen in the output, a caller from another module can access TypeWithSubject.publisher as its native type. This means you can’t change your publisher to a different type without breaking the caller. By comparison, TypeWithErasedSubject.publisher appears to callers as an AnyPublisher, so you can change the underlying publisher type at will.
DeepL
次の例では、それぞれがパブリッシャーのプロパティを持つ2つのタイプを示しています。TypeWithSubject はこのプロパティを実際の型である PassthroughSubject として公開し、TypeWithErasedSubject は eraseToAnyPublisher() を使用して AnyPublisher として公開しています。出力に見られるように、他のモジュールからの呼び出し元は、TypeWithSubject.publisherをそのネイティブな型としてアクセスすることができます。つまり、呼び出し元を壊すことなくパブリッシャーを別の型に変更することはできません。それに比べて、TypeWithErasedSubject.publisher は呼び出し元には AnyPublisher として表示されるので、根本的なパブリッシャーのタイプを自由に変更することができます。
AnyPublisherの公式リファレンスより
原文
Use AnyPublisher to wrap a publisher whose type has details you don’t want to expose across API boundaries, such as different modules. Wrapping a Subject with AnyPublisher also prevents callers from accessing its send(_:) method. When you use type erasure this way, you can change the underlying publisher implementation over time without affecting existing clients.
DeepL
AnyPublisher は、異なるモジュールなど、API の境界を越えて公開したくない詳細情報を持つパブリッシャーをラップするために使用します。AnyPublisher で Subject をラップすると、呼び出し元が send(_:) メソッドにアクセスできなくなります。このようにして型の消去を行うと、既存のクライアントに影響を与えることなく、 徐々にパブリッシャーの実装を変更することができます。
原文
You can use Combine’s eraseToAnyPublisher() operator to wrap a publisher with AnyPublisher.
DeepL
Combine の eraseToAnyPublisher() 演算子を使って、パブリッシャーを AnyPublisher でラップすることができます。
要約
- Publisherのインスタンスを公開するには実際の型ではなくeraseToAnyPublisher() を使用して抽象化した型を提供する
- 抽象化されているので利用する側に影響を与えることなくPublisherの提供側の実装を変更することが可能になる
- AnyPublisherはモジュール境界などを越えて公開したくない詳細情報を持つパブリッシャーをラップするために使用する(詳細を隠蔽したい時に使用する)
- 例えば、AnyPublisher で Subject をラップするとPublisherの利用側では
send(_:)
メソッドにアクセスできなくなる
-
promise(.failure())
をcatch
しない場合はreceiveCompletion:
が必要 -
print(type(of: publisher))
するとFuture<String, MyError>
となっている -
print(type(of: publisher.eraseToAnyPublisher()))
してもAnyPublisher<String, MyError>
となり、AnyPublisher
のFailure
がMyError
なのでエラー処理が必要であることがわかる
import Combine
struct MyError: Error {
var description: String
}
let publisher = Future<String, MyError> { promise in
//promise(.success("Hello"))
promise(.failure(MyError(description: "エラー")))
}
print(type(of: publisher))
print(type(of: publisher.eraseToAnyPublisher()))
let cancellable = publisher.sink(receiveCompletion: { completion in
switch completion {
case .finished:
print("finished")
case .failure(let error):
print("error \(error.description)")
}
}, receiveValue: { message in
print("received message: \(message)")
})
出力
Future<String, MyError>
AnyPublisher<String, MyError>
error エラー
-
promise(.failure())
をcatch
すると.success()
に落ちる -
print(type(of: publisher))
するとCatch<Future<String, MyError>, Just<String>>
になる -
print(type(of: publisher.eraseToAnyPublisher()))
するとAnyPublisher<String, Never>
となり、AnyPublisher
のFailure
がNever
なのでエラー処理が不要であることがわかる - エラーにならないので
sink
では正常処理だけ実装すれば良い
import Combine
struct MyError: Error {
var description: String
}
let publisher = Future<String, MyError> { promise in
//promise(.success("Hello"))
promise(.failure(MyError(description: "エラー")))
}.catch{ error -> Just<String> in
Just("error:\(error.description)")
}
print(type(of: publisher))
print(type(of: publisher.eraseToAnyPublisher()))
let cancellable = publisher.sink { value in
print("value: \(value)")
}
Catch<Future<String, MyError>, Just<String>>
AnyPublisher<String, Never>
value: error:エラー
replaceError
import Combine
struct MyError: Error {
var description: String
}
let publisher = Future<String, MyError> { promise in
promise(.failure(MyError(description: "エラー")))
}
print(type(of: publisher))
print(type(of: publisher.eraseToAnyPublisher()))
let cancellable = publisher.replaceError(with: "エラーです").sink { value in
print("value: \(value)")
}
Future<String, MyError>
AnyPublisher<String, MyError>
value: エラーです
Operator
- switchToLatest
- 連続して流れたりキャンセルするようなケースで最新の値だけ取り出せば良い時に使う
@Published
変数への代入をCombineで処理する
import Combine
final class CityViewModel {
@Published var city: String = ""
private var disposables = Set<AnyCancellable>()
init() {
$city.dropFirst().sink(receiveValue: { print("\($0), 2020") }).store(in: &disposables)
}
deinit {
disposables.removeAll()
}
}
let viewModel = CityViewModel()
viewModel.city = "Tokyo"
/* Prints:
Tokyo, 2020
*/
この例ならfilter()
の方が良いかも
import Combine
final class CityViewModel {
@Published var city: String = ""
private var disposables = Set<AnyCancellable>()
init() {
$city.filter{!$0.isEmpty}.sink{ print("\($0), 2020") }.store(in: &disposables)
}
deinit {
disposables.removeAll()
}
}
let viewModel = CityViewModel()
viewModel.city = "Tokyo"
decode()
が返すError
を独自の型に変換したい時
このように書いても良いし
.decode(type: SearchResponse.self, decoder: decoder())
.mapError { error in
switch error {
case is Swift.DecodingError:
return .decodeError(error)
case let error as APIError:
return error
default:
return .unknownError(error)
}
}
.eraseToAnyPublisher()
このようにも書ける
.decode(type: SearchResponse.self, decoder: decoder())
.mapError { $0 as? APIError ?? .decodeError($0) }
.eraseToAnyPublisher()
全体
//
// GitHubAPI.swift
// GitHubRepositorySearchSwiftUI
//
// Created by yorifuji on 2021/06/07.
//
import Combine
import Foundation
enum APIError: Error {
case createURLError
case urlError(URLError)
case responseError
case statusError(Int)
case decodeError(Error)
case unknownError(Error)
}
protocol GitHubAPIProtocol {
static func searchRepository(_ query: String) -> AnyPublisher<SearchResponse, APIError>
}
enum GitHubAPI: GitHubAPIProtocol {
private static func decoder() -> JSONDecoder {
let decoder: JSONDecoder = .init()
decoder.keyDecodingStrategy = .convertFromSnakeCase
return decoder
}
static func searchRepository(_ query: String) -> AnyPublisher<SearchResponse, APIError> {
let baseURL = "https://api.github.com/search/repositories?q="
guard let url = URL(string: baseURL + query) else {
return Fail(error: APIError.createURLError).eraseToAnyPublisher()
}
return URLSession.shared.dataTaskPublisher(for: url)
.mapError { error in
APIError.urlError(error)
}
.tryMap { (data, response) -> Data in
guard let httpRes = response as? HTTPURLResponse else {
throw APIError.responseError
}
if (200..<300).contains(httpRes.statusCode) == false {
throw APIError.statusError(httpRes.statusCode)
}
return data
}
.decode(type: SearchResponse.self, decoder: decoder())
.mapError { $0 as? APIError ?? .decodeError($0) }
.eraseToAnyPublisher()
}
}
Combineと関係ないけどstaticなオブジェクトを生成して使い回せば良いときは後者のような書き方ができる
private static func decoder() -> JSONDecoder {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
return decoder
}
private static let decorder: JSONDecoder = {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
return decoder
}()
Back Pressure
debounce
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
import Foundation
import Combine
let bounces:[(Int,TimeInterval)] = [
(0, 0),
(1, 0.25), // 0.25s interval since last index
(2, 1), // 0.75s interval since last index
(3, 1.25), // 0.25s interval since last index
(4, 1.5), // 0.25s interval since last index
(5, 2) // 0.5s interval since last index
]
let subject = PassthroughSubject<Int, Never>()
let cancellable = subject
.print("dump")
.debounce(for: .seconds(0.5), scheduler: RunLoop.main)
.sink { index in
print ("Received index \(index)")
}
for bounce in bounces {
DispatchQueue.main.asyncAfter(deadline: .now() + bounce.1) {
subject.send(bounce.0)
}
}
dump: receive subscription: (PassthroughSubject)
dump: request unlimited
dump: receive value: (0)
dump: receive value: (1)
Received index 1
dump: receive value: (2)
dump: receive value: (3)
dump: receive value: (4)
Received index 4
dump: receive value: (5)
Received index 5
collect
import Combine
let numbers = (0...10)
let cancellable = numbers.publisher
.collect(5)
.sink { print("\($0)", terminator: " ") }
[0, 1, 2, 3, 4] [5, 6, 7, 8, 9] [10]
throttle
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
import Foundation
import Combine
let cancellable = Timer.publish(every: 1.0, on: .main, in: .default)
.autoconnect()
.print("\(Date().description)")
.throttle(for: 5.0, scheduler: RunLoop.main, latest: true)
.sink(
receiveCompletion: { print ("Completion: \($0).") },
receiveValue: { print("Received Timestamp \($0).") }
)
2021-06-13 06:57:51 +0000: receive subscription: ((extension in Foundation):__C.NSTimer.TimerPublisher.Inner<Combine.Publishers.Autoconnect<(extension in Foundation):__C.NSTimer.TimerPublisher>.(unknown context at $7fff2e6301a0).Inner<Combine.Publishers.Print<Combine.Publishers.Autoconnect<(extension in Foundation):__C.NSTimer.TimerPublisher>>.(unknown context at $7fff2e6304e0).Inner<Combine.Publishers.Throttle<Combine.Publishers.Print<Combine.Publishers.Autoconnect<(extension in Foundation):__C.NSTimer.TimerPublisher>>, __C.NSRunLoop>.(unknown context at $7fff2e6375b0).Inner<Combine.Subscribers.Sink<Foundation.Date, Swift.Never>>>>>)
2021-06-13 06:57:51 +0000: request unlimited
2021-06-13 06:57:51 +0000: receive value: (2021-06-13 06:57:52 +0000)
Received Timestamp 2021-06-13 06:57:52 +0000.
2021-06-13 06:57:51 +0000: receive value: (2021-06-13 06:57:53 +0000)
2021-06-13 06:57:51 +0000: receive value: (2021-06-13 06:57:54 +0000)
2021-06-13 06:57:51 +0000: receive value: (2021-06-13 06:57:55 +0000)
2021-06-13 06:57:51 +0000: receive value: (2021-06-13 06:57:56 +0000)
2021-06-13 06:57:51 +0000: receive value: (2021-06-13 06:57:57 +0000)
Received Timestamp 2021-06-13 06:57:57 +0000.
2021-06-13 06:57:51 +0000: receive value: (2021-06-13 06:57:58 +0000)
2021-06-13 06:57:51 +0000: receive value: (2021-06-13 06:57:59 +0000)
2021-06-13 06:57:51 +0000: receive value: (2021-06-13 06:58:00 +0000)
2021-06-13 06:57:51 +0000: receive value: (2021-06-13 06:58:01 +0000)
2021-06-13 06:57:51 +0000: receive value: (2021-06-13 06:58:02 +0000)
Received Timestamp 2021-06-13 06:58:02 +0000.
2021-06-13 06:57:51 +0000: receive value: (2021-06-13 06:58:03 +0000)
2021-06-13 06:57:51 +0000: receive value: (2021-06-13 06:58:04 +0000)
2021-06-13 06:57:51 +0000: receive value: (2021-06-13 06:58:05 +0000)
2021-06-13 06:57:51 +0000: receive value: (2021-06-13 06:58:06 +0000)
2021-06-13 06:57:51 +0000: receive value: (2021-06-13 06:58:07 +0000)
Received Timestamp 2021-06-13 06:58:07 +0000.
flatMap
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
import Foundation
import Combine
struct Repository {
public let name: String
}
struct Response: Decodable {
var total_count: Int
}
let publisher = PassthroughSubject<Repository, Never>()
let cancellable = publisher
.flatMap { repository -> URLSession.DataTaskPublisher in
let url = URL(string:"https://api.github.com/search/repositories?q=\(repository.name)")!
return URLSession.shared.dataTaskPublisher(for: url)
}
.map { $0.data }
.decode(type: Response.self, decoder: JSONDecoder())
.sink(
receiveCompletion: { print($0) },
receiveValue: { print($0) }
)
publisher.send(Repository(name: "swift"))
publisher.send(Repository(name: "apple"))
Response(total_count: 209609)
Response(total_count: 55463)