[Swift5][Combine] collect()を使ってPublisherの配列から出力をまとめた配列を返すPublisherを作成する

## TL;DR
- Publisherの配列があり、全てのPublisherの出力をまとめた配列を返すPublisherが欲しかったら`collect()`を使おう。
- 結果が２次元配列になってしまう場合はflatMapで１次元化しよう。

## モチベーション
Publisherの配列から結果をまとめて返してくれるPublisherを作ろうと思いました。

例えばFirebaseのStorageからReferenceを取得する場面があるとします。
Referenceは以下のパスで画像を保存しています。

```
root
└── Store // お店の画像を保存するパス
    ├── Header // ヘッダー画像のパス
    │    └──{ID} // 画像
    └── Image // その他の画像のパス
         ├── UserA // Userごとのパス
         │   └──{ID} // 画像
         ├── UserB
         │   └──{ID}
         └── UserC
             └──{ID}
```

`お店`というエンティティに対し、ヘッダー画像とその他の画像があります。
その他の画像はいろんなユーザーが投稿できるためユーザーごとにディレクトリを分けています。
(この設計が正しいかどうかは分かりません。。。)

Headerのようにreferenceから直下の画像を取得する時はこんな感じでReferenceを取得するFutureを作ってやれば可能です。

```swift
extension StorageReference {

    func getReferences() -> AnyPublisher<[StorageReference], Error> {

        Deferred {
            Future<[StorageReference], Error> { [weak self] promise in

                guard let self = self else { return }

                self.listAll { result, error in

                    if let error = error {
                        promise(.failure(error))
                        return
                    }

                    promise(.success(result.items))
                }
            }

        }.eraseToAnyPublisher()
    }
}

// Usage
Storage.storage().reference(withPath: "パス")
    .getReferences()
    .sink(
        receiveCompletion: { result in
            // なんかする
        }, receiveValue: { references in
            // なんかする
        }
    )
    .store(in: &cancellables)
```


けどImageのreferenceを取得してくるのはちょっとめんどくさいです。
単純にImage直下のReferenceを取得してきても、UserA,B,CのReferenceがPrefixとして取得されるだけなので、画像のReferenceは取得できません。
画像を取得するには取得したUserA、B、CのReferenceそれぞれに対して、listAllを呼ぶ必要があります。

## forEachで愚直にやってみた場合
まずはImage配下から取得できるのがitemではなくprefixなので、prefixが取得できた時はPrefixを返すように先程のFutureを変更します。

```swift
    func getReferences() -> AnyPublisher<[StorageReference], Error> {

        Deferred {
            Future<[StorageReference], Error> { [weak self] promise in

                guard let self = self else { return }

                self.listAll { result, error in

                    if let error = error {
                        promise(.failure(error))
                        return
                    }

                    if result.prefixes.isEmpty {
                        promise(.success(result.items))
                    } else {
                        promise(.success(result.prefixes)) // UserA, B, Cのreferenceが返ってくる。
                    }
                }
            }

        }.eraseToAnyPublisher()
    }
```

こうして返されるのがRefenrenceの配列で、これらをまた画像の取得用のPublisherに変換するとPublisherの配列ができます。
そのPublisherの配列をforEachでそれぞれsubscribeしていくと以下のようになります。

```swift
Storage.storage().reference(withPath: "パス")
    .getReferences()
    .catch { _ -> Just<[StorageManager.Reference]> in
        return .init([])
    }
    .sink { prefixes in
        prefixes
            .map { $0.getReferences() }
            .forEach {
                $0.catch { _ -> Just<[StorageManager.Reference]> in
                    return .init([])
                }
                .sink { [weak self] references in

                    guard let self = self else { return }

                    self.imageReferences.append(contentsOf: references)
                }
                .store(in: &self.cancellables)
           }
    }
    .store(in: &cancellables)

```

上記の書き方でもUserA、B、Cのパス全ての画像を取得してくることは可能です。
でもちょっと嫌ですよね。。。
ネスト深いし、`.store(in: )`を何度も呼んでいてどこで何をsubscribeしてるのか読みにくいです。


## collectというOperatorを使おう
上記のコードは`collect()`というOperatorを使うと解決できます。

https://developer.apple.com/documentation/combine/fail/collect()

![名称未設定.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311282/1b635bac-72fd-71ff-20eb-ab99860ab717.jpeg)

`collect()`は複数の出力を配列にして出力してくれるOperatorです。

※0~10を出力するPublisherをIntの配列を出力するPublisherに変換する例

```swift
import Combine

var cancellables = Set<AnyCancellable>()

(0...10).publisher
    .handleEvents(receiveOutput: {
        print($0)
    })
    .collect()
    .sink { print("\($0)") }
    .store(in: &cancellables)
```
結果

```
0
1
2
3
4
5
6
7
8
9
10
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
```

途中までは数字を個別に出力していますが、collectを噛ませると全てまとめて配列にして出力してくれることが分かります。

これを先程のStorageReferenceを使うケースで使ってみました。

結論以下の形になります。

```swift
Storage.storage().reference(withPath: "パス")
    .getReferences()
    .catch { _ -> Just<[StorageManager.Reference]> in
        return .init([])
    }
    .flatMap {
        $0.publisher // [StorageReference]の要素を出力するpublisherを作成
            .flatMap { $0.getReferences() } // 配下の画像のreferenceを取得するAnyPublisherに変換
            .collect() // collectで全ての出力をまとめて配列にするPublisherに変換
        }
    .map { $0.flatMap { $0 } } // [[StorageReference]]として返ってくるのでflatMapで１次元配列にする。
    .sink(
        receiveCompletion: { result in
            // なんかする
        }, receiveValue: { references in
            // なんかする
        }
    )
    .store(in: &cancellables)
```

コメントで説明しているように、StorageReferenceを個別にSinkするのではなく、Collectを使って出力を配列化して返すPublisherを作成し、flatMapで変換しています。
今回はcollectでまとめた出力が元々配列だったことで２次元配列になってしまったため、出力結果をflatMapにかけるmapを噛ませて(ややこしい。。。)１次元配列に直しています。

これで個別にPublisherをSinkしなくても結果をまとめてSubscribeできるようになりました。

## まとめ
- Publisherの配列の出力をまとめるときはcollectが使える。
- 配列が多次元になる時はflatMapで１次元になおそう。

## おまけ
毎回flatMapとcollectで変換するのも大変なのでArrayのextensionで変換したPublisherを作成できるようにしてみました。

```swift
extension Array where Element: StorageReference {

    func getReferences() -> AnyPublisher<[StorageReference], Error> {
        publisher
            .flatMap { $0.getReferences() }
            .collect()
            .map { $0.flatMap { $0 } }
            .eraseToAnyPublisher()
    }

}

Storage.storage().reference(withPath: "パス")
    .getReferences()
    .catch { _ -> Just<[StorageManager.Reference]> in
        return .init([])
    }
    .flatMap { $0.getReferences() }
    .sink(
        receiveCompletion: { result in
            // なんかする
        }, receiveValue: { references in
            // なんかする
        }
    )
    .store(in: &cancellables)
```

だいぶスッキリしたと思います。

欲を言えばstorageを取得した時にprefixがあれば再帰的に画像を取得するようにしたかったんですが、難しくて諦めました。。。
アルゴリズムに強くなりたい。
元気がある時に再チャレンジしてみようと思います。