Closed18

Combine

yorifujiyorifuji
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
yorifujiyorifuji
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
yorifujiyorifuji
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
yorifujiyorifuji
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]
*/
yorifujiyorifuji
import Combine

let subject = PassthroughSubject<Int, Never>()
var cancellable = subject.sink{ print($0) }
subject.send(1)
subject.send(completion: .finished)

/* Prints:
1
*/
yorifujiyorifuji

eraseToAnyPublisher()AnyPublisherについて

eraseToAnyPublisherの公式リファレンスより
https://developer.apple.com/documentation/combine/publisher/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の公式リファレンスより
https://developer.apple.com/documentation/combine/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(_:) メソッドにアクセスできなくなる
yorifujiyorifuji
  • promise(.failure())catchしない場合はreceiveCompletion:が必要
  • print(type(of: publisher))するとFuture<String, MyError>となっている
  • print(type(of: publisher.eraseToAnyPublisher()))してもAnyPublisher<String, MyError>となり、AnyPublisherFailureMyErrorなのでエラー処理が必要であることがわかる
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 エラー
yorifujiyorifuji
  • promise(.failure())catchすると.success()に落ちる
  • print(type(of: publisher))するとCatch<Future<String, MyError>, Just<String>>になる
  • print(type(of: publisher.eraseToAnyPublisher()))するとAnyPublisher<String, Never>となり、AnyPublisherFailureNeverなのでエラー処理が不要であることがわかる
  • エラーにならないので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:エラー
yorifujiyorifuji

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: エラーです
yorifujiyorifuji

Operator

  • switchToLatest
    • 連続して流れたりキャンセルするようなケースで最新の値だけ取り出せば良い時に使う
yorifujiyorifuji

@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"
yorifujiyorifuji

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()
    }
}


yorifujiyorifuji

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
}()
yorifujiyorifuji

debounce

https://developer.apple.com/documentation/combine/publisher/debounce(for:scheduler:options:)


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
yorifujiyorifuji

throttle

https://developer.apple.com/documentation/combine/publisher/throttle(for:scheduler:latest:)


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.
yorifujiyorifuji

flatMap

https://developer.apple.com/documentation/combine/publisher/flatmap(maxpublishers:_:)-3k7z5


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)
このスクラップは2021/07/31にクローズされました