【Swift】Combineについて

# はじめに
Combineについて、ふわっとした理解でここまできてしまったので、一度公式ドキュメントを読んでみました。

https://developer.apple.com/documentation/combine/receiving-and-handling-events-with-combine

# 内容
### [Publisher](https://developer.apple.com/documentation/combine/publisher)を[Subscriber](https://developer.apple.com/documentation/combine/subscriber)に接続する

Publisherの生成は、[`publisher(for:object:)`](https://developer.apple.com/documentation/foundation/notificationcenter/3329353-publisher)を使う
```swift
func publisher(
    for name: Notification.Name,
    object: AnyObject? = nil
) -> NotificationCenter.Publisher
```

例えばこのような形で
```swift
let sub = NotificationCenter.default
    .publisher(for: NSControl.textDidChangeNotification, object: filterField)
    .sink(receiveCompletion: { print ($0) },
          receiveValue: { print ($0) })
```

Combineは2つの組み込みの[Subscriber](https://developer.apple.com/documentation/combine/subscriber)を提供
##### ■[`sink(receiveCompletion:receiveValue:)`](https://developer.apple.com/documentation/combine/publisher/sink(receivecompletion:receivevalue:))

```swift
let myRange = (0...3)
cancellable = myRange.publisher
    .sink(receiveCompletion: { print ("completion: \($0)") },
          receiveValue: { print ("value: \($0)") })

// Prints:
//  value: 0
//  value: 1
//  value: 2
//  value: 3
//  completion: finished
```

2つのクロージャーをとる
- 最初のクロージャーは、パブリッシャーが正常に終了したかエラーで失敗したかを示す[`Completion`](https://developer.apple.com/documentation/combine/subscribers/completion)を受け取ったときに実行される
- 2番目のクロージャは、パブリッシャーから要素を受け取ったときに実行される

##### ■[`assign(to:on:)`](https://developer.apple.com/documentation/combine/publisher/assign(to:on:))

```swift
class MyClass {
    var anInt: Int = 0 {
        didSet {
            print("anInt was set to: \(anInt)", terminator: "; ")
        }
    }
}

var myObject = MyClass()
let myRange = (0...2)
cancellable = myRange.publisher
    .assign(to: \.anInt, on: myObject)

// Prints: "anInt was set to: 0; anInt was set to: 1; anInt was set to: 2"
```

受け取ったすべての要素を、与えられたオブジェクトのプロパティに即座に割り当て、プロパティを示すキーパスを使用

### Operatorを使用して型を変換する

```swift
let sub = NotificationCenter.default
    .publisher(for: NSControl.textDidChangeNotification, object: filterField)
    .map( { ($0.object as! NSTextField).stringValue } )
    .assign(to: \MyViewModel.filterString, on: myViewModel)
```

#### [`map(_:)`](https://developer.apple.com/documentation/combine/publisher/map(_:)-99evh)
パブリッシャーからの全ての要素を、指定されたクロージャで変換

```swift
let numbers = [5, 4, 3, 2, 1, 0]
let romanNumeralDict: [Int : String] =
   [1:"I", 2:"II", 3:"III", 4:"IV", 5:"V"]
cancellable = numbers.publisher
    .map { romanNumeralDict[$0] ?? "(unknown)" }
    .sink { print("\($0)", terminator: " ") }

// Prints: "V IV III II I (unknown)"
```

#### [`flatMap(maxPublishers:_:)`](https://developer.apple.com/documentation/combine/publisher/flatmap(maxpublishers:_:)-3k7z5)
パブリッシャーから、指定した最大数のパブリッシャーまで、すべての要素を新しいパブリッシャーに変換

```swift
public struct WeatherStation {
    public let stationID: String
}

var weatherPublisher = PassthroughSubject<WeatherStation, URLError>()

cancellable = weatherPublisher.flatMap { station -> URLSession.DataTaskPublisher in
    let url = URL(string:"https://weatherapi.example.com/stations/\(station.stationID)/observations/latest")!
    return URLSession.shared.dataTaskPublisher(for: url)
}
.sink(
    receiveCompletion: { completion in
        // Handle publisher completion (normal or error).
    },
    receiveValue: {
        // Process the received data.
    }
 )

weatherPublisher.send(WeatherStation(stationID: "KSFO")) // San Francisco, CA
weatherPublisher.send(WeatherStation(stationID: "EGLC")) // London, UK
weatherPublisher.send(WeatherStation(stationID: "ZBBB")) // Beijing, CN
```

#### [`reduce(_:_:)`](https://developer.apple.com/documentation/combine/publisher/reduce(_:_:))
ストリームの各要素を収集し、完了時に最終結果を公開するクロージャを適用

```swift
let numbers = (0...10)
cancellable = numbers.publisher
    .reduce(0, { accum, next in accum + next })
    .sink { print("\($0)") }

// Prints: "55"
```

### Operatorを使用してPublisherをカスタマイズする

```swift
let sub = NotificationCenter.default
    .publisher(for: NSControl.textDidChangeNotification, object: filterField)
    .map( { ($0.object as! NSTextField).stringValue } )
    .filter( { $0.unicodeScalars.allSatisfy({CharacterSet.alphanumerics.contains($0)}) } )
    .debounce(for: .milliseconds(500), scheduler: RunLoop.main)
    .receive(on: RunLoop.main)
    .assign(to:\MyViewModel.filterString, on: myViewModel)
```

#### [`filter(_:)`](https://developer.apple.com/documentation/combine/publisher/filter(_:))
指定されたクロージャにマッチするすべての要素を再パブリッシュ

```swift
let numbers: [Int] = [1, 2, 3, 4, 5]
cancellable = numbers.publisher
    .filter { $0 % 2 == 0 }
    .sink { print("\($0)", terminator: " ") }

// Prints: "2 4"
```

#### [`debounce(for:scheduler:options:)`](https://developer.apple.com/documentation/combine/publisher/debounce(for:scheduler:options:))
イベントとイベントの間に指定された時間間隔が経過した後にのみ、要素を公開

```swift
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>()
cancellable = subject
    .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)
    }
}

// Prints:
//  Received index 1
//  Received index 4
//  Received index 5

//  Here is the event flow shown from the perspective of time, showing value delivery through the `debounce()` operator:

//  Time 0: Send index 0.
//  Time 0.25: Send index 1. Index 0 was waiting and is discarded.
//  Time 0.75: Debounce period ends, publish index 1.
//  Time 1: Send index 2.
//  Time 1.25: Send index 3. Index 2 was waiting and is discarded.
//  Time 1.5: Send index 4. Index 3 was waiting and is discarded.
//  Time 2: Debounce period ends, publish index 4. Also, send index 5.
//  Time 2.5: Debounce period ends, publish index 5.
```

#### [`receive(on:options:)`](https://developer.apple.com/documentation/combine/publisher/receive(on:options:))
パブリッシャーからエレメントを受け取るスケジューラーを指定

```swift
let jsonPublisher = MyJSONLoaderPublisher() // Some publisher.
let labelUpdater = MyLabelUpdateSubscriber() // Some subscriber that updates the UI.

jsonPublisher
    .subscribe(on: backgroundQueue)
    .receive(on: RunLoop.main)
    .subscribe(labelUpdater)
```

### 必要なときにパブリッシングをキャンセルする

パブリッシャーは、正常に完了するか失敗するまで要素を発行し続けるが、購読する必要がなくなった場合は、購読をキャンセルできる

#### [`Cancellable`](https://developer.apple.com/documentation/combine/cancellable)
-> `sub?.cancel()`

## その他のOperator
#### Mapping Elements
##### [`mapError(_:)`](https://developer.apple.com/documentation/combine/publisher/maperror(_:))
パブリッシャーからのあらゆる失敗を、新しいエラーに変換

##### [`replaceNil(with:)`](https://developer.apple.com/documentation/combine/publisher/replacenil(with:))
ストリーム中のnil要素を与えられた要素で置き換え

##### [`scan(_:_:)`](https://developer.apple.com/documentation/combine/publisher/scan(_:_:))
クロージャーが最後に返した値と共に現在の要素をクロージャーに提供することで、 パブリッシャーから要素を変換

##### [`setFailureType(to:)`](https://developer.apple.com/documentation/combine/publisher/setfailuretype(to:))
パブリッシャーが宣言したFailureTypeを変更

#### Filtering Elements
##### [`compactMap(_:)`](https://developer.apple.com/documentation/combine/publisher/compactmap(_:))
受け取った各要素でクロージャを呼び出し、値を持つ返されたオプショナルを公開

##### [`removeDuplicates()`](https://developer.apple.com/documentation/combine/publisher/removeduplicates())
前の要素と一致しない要素のみをパブリッシュ

##### [`removeDuplicates(by:)`](https://developer.apple.com/documentation/combine/publisher/removeduplicates(by:))
指定されたクロージャによって評価された、前の要素にマッチしない要素のみをパブリッシュ

##### [`replaceEmpty(with:)`](https://developer.apple.com/documentation/combine/publisher/replaceempty(with:))
空のストリームを与えられた要素で置き換え

##### [`replaceError(with:)`](https://developer.apple.com/documentation/combine/publisher/replaceerror(with:))
ストリームのあらゆるエラーを、提供された要素で置き換え

#### Reducing Elements
##### [`collect()`](https://developer.apple.com/documentation/combine/publisher/collect())
受信した全ての要素を収集し、パブリッシャーが終了した時点でその収集した要素の配列を一つ出力

##### [`collect(_:)`](https://developer.apple.com/documentation/combine/publisher/collect(_:))
指定された要素数まで収集し、そのコレクションの配列を1つ出力

##### [`collect(_:options:)`](https://developer.apple.com/documentation/combine/publisher/collect(_:options:))
指定された時間グループ化戦略によって要素を収集し、コレクションの単一の配列を出力

##### [`ignoreOutput()`](https://developer.apple.com/documentation/combine/publisher/ignoreoutput())
すべてのアップストリームエレメントを無視しますが、アップストリームパブリッシャーの完了状態 (finished または failed)は渡す

#### Applying Mathematical Operations on Elements
##### [`count()`](https://developer.apple.com/documentation/combine/publisher/count())
パブリッシャーから受信したエレメント数をパブリッシュ

##### [`max()`](https://developer.apple.com/documentation/combine/publisher/max())
パブリッシャーから受信した最大値を、終了後にパブリッシュ

##### [`max(by:)`](https://developer.apple.com/documentation/combine/publisher/max(by:))
パブリッシャーから受け取った最大値を、提供された順序付けクロージャを使用してパブリッシュ

##### [`min()`](https://developer.apple.com/documentation/combine/publisher/min())
パブリッシャーから受け取った最小値を、終了後にパブリッシュ

##### [`min(by:)`](https://developer.apple.com/documentation/combine/publisher/min(by:))
パブリッシャーから受け取った最小値を、終了後にパブリッシュ

#### Applying Matching Criteria to Elements
##### [`contains(_:)`](https://developer.apple.com/documentation/combine/publisher/contains(_:))
引数に等しい要素を受け取った場合、Boolを発行

##### [`contains(where:)`](https://developer.apple.com/documentation/combine/publisher/contains(where:))
条件を満たす要素を受信すると、Boolを発行

##### [`allSatisfy(_:)`](https://developer.apple.com/documentation/combine/publisher/allsatisfy(_:))
受信したすべての要素が指定された条件を満たすかどうかを示す単一のBoolを発行

#### Applying Sequence Operations to Elements
##### [`drop(untilOutputFrom:)`](https://developer.apple.com/documentation/combine/publisher/drop(untiloutputfrom:))
2番目のパブリッシャーからエレメントを受け取るまで、アップストリームパブリッシャーからのエレメントを無視

##### [`dropFirst(_:)`](https://developer.apple.com/documentation/combine/publisher/dropfirst(_:))
指定された数の要素を省略して、それ以降の要素を再パブリッシュ

##### [`drop(while:)`](https://developer.apple.com/documentation/combine/publisher/drop(while:))
クロージャがfalseを返すまで、パブリッシャーから要素を削除し、その後、残りのすべての要素を再パブリッシュ

##### [`append(_:)`](https://developer.apple.com/documentation/combine/publisher/append(_:)-1qb8d)
パブリッシャーの出力に、指定した要素を追加

##### [`prefix(_:)`](https://developer.apple.com/documentation/combine/publisher/prefix(_:))
指定された最大数まで要素を再パブリッシュ

##### [`prefix(while:)`](https://developer.apple.com/documentation/combine/publisher/prefix(while:))
条件を満たす間、要素を再パブリッシュ

##### [`prefix(untilOutputFrom:)`](https://developer.apple.com/documentation/combine/publisher/prefix(untiloutputfrom:))
他のパブリッシャーが要素を発行するまで、要素を再パブリッシュ

#### Selecting Specific Elements
##### [`first()`](https://developer.apple.com/documentation/combine/publisher/first())
ストリームの最初の要素をパブリッシュし、終了

##### [`first(where:)`](https://developer.apple.com/documentation/combine/publisher/first(where:))
条件を満たす最初の要素をパブリッシュし、その後、通常通り終了

##### [`last()`](https://developer.apple.com/documentation/combine/publisher/last())
ストリームの終了後、ストリームの最後の要素をパブリッシュ

##### [`last(where:)`](https://developer.apple.com/documentation/combine/publisher/last(where:))
条件を満たす最後の要素を、上流が終了した後にパブリッシュ

##### [`output(at:)`](https://developer.apple.com/documentation/combine/publisher/output(at:))
公開された要素の特定のインデックスの要素を公開

##### [`output(in:)`](https://developer.apple.com/documentation/combine/publisher/output(in:))
公開された要素のシーケンスで、その範囲で指定された要素を公開

#### Collecting and Republishing the Latest Elements from Multiple Publishers
##### [`combineLatest(_:)`](https://developer.apple.com/documentation/combine/publisher/combinelatest(_:))
追加のパブリッシャーにサブスクライブし、どちらかのパブリッシャーから出力を受けたときにタプルをパブリッシュ

#### Republishing Elements from Multiple Publishers as an Interleaved Stream
##### [`merge(with:)`](https://developer.apple.com/documentation/combine/publisher/merge(with:)-7qt71)
パブリッシャーからの要素と他のパブリッシャーからの要素を組み合わせて、一連の要素を配信

#### Collecting and Republishing the Oldest Unconsumed Elements from Multiple Publishers
##### [`zip(_:)`](https://developer.apple.com/documentation/combine/publisher/zip(_:))
他のパブリッシャーからの要素を結合し、要素のペアをタプルとして配信

#### Republishing Elements by Subscribing to New Publishers
##### [`switchToLatest()`](https://developer.apple.com/documentation/combine/publisher/switchtolatest()-453ht)
直近に受信したパブリッシャーから送信されたエレメントを再パブリッシュ

#### Performing Type-Erasure
##### [`eraseToAnyPublisher()`](https://developer.apple.com/documentation/combine/publisher/erasetoanypublisher())
型消去により、異なるモジュールのようなAPIの境界を越えて抽象化を維持することができる

```swift
public class TypeWithSubject {
    public let publisher: some Publisher = PassthroughSubject<Int,Never>()
}
public class TypeWithErasedSubject {
    public let publisher: some Publisher = PassthroughSubject<Int,Never>()
        .eraseToAnyPublisher()
}

// In another module:
let nonErased = TypeWithSubject()
if let subject = nonErased.publisher as? PassthroughSubject<Int,Never> {
    print("Successfully cast nonErased.publisher.")
}
let erased = TypeWithErasedSubject()
if let subject = erased.publisher as? PassthroughSubject<Int,Never> {
    print("Successfully cast erased.publisher.")
}

// Prints "Successfully cast nonErased.publisher."
```

# おわりに
ドキュメントを読んだことでCombineの整理が少し進みましたが、オペレーターについてはここに記載した以外にもまだあるので、引き続き整理していきたいと思います。


# 参考
https://developer.apple.com/documentation/combine/receiving-and-handling-events-with-combine