Swift CombineをAsyncSequenceとして処理する

# AsyncSequenceについて
Swiftで提供される宣言型 APIとしてCombineが、iOSではiOS 13から提供されるようになりました。
しかしながら、CombineはiOSやmacOSなどに提供されるフレームワークで、Server Side Swiftなどでは使用できませんでした。

AsyncSequenceはSwift Standard Libraryに含まれるAPIで、Combineよりも機能は少ないものの、
- プラットフォームに依存しない
- for awaitを用いて処理が可能

などの特徴があります。

例えばAsyncSequenceに適合するAsyncStreamを作成して、filterをかけて偶数しか出力しないように加工して、for awaitでStateに設定して偶数を表示したりできます。

```swift
struct CounterView: View {
    @State private var count = 0
    var body: some View {
        Text(count, format: .currency(code: "JPY"))
            .task {
                let even = countUp()
                    .filter { $0 % 2 == 0 }
                for await i in even {
                    count = i
                }
            }
    }
    
    func countUp() -> AsyncStream<Int> {
        .init { continuation in
            var count = 1
            Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
                count += 1
                continuation.yield(count)
            }
        }
    }
}
```

# CombineからAsyncSequenceに変換する

CombineのストリームをAsyncSequenceとして処理するには、`values` を使います。

https://developer.apple.com/documentation/combine/publisher/values-1dm9r

valuesが返すのはAsyncPublisherもしくはAsyncThrowingPublisherで、どちらもAsyncSequenceに適合しているので、returnで返すことによりfor await(for try await)で処理をすることが可能です。

以下は地図の位置情報から地名に変換して文字列として返すSwiftUIです。地図を動かすたびにcoordinateRegionが変更され、その位置情報からリバースジオコーディングして地名に変換する処理をCombineで構成しています。

CLGeocoderなどのエラーはnilにしてcompactMapでフィルタリングしているためエラーはNeverになることと、valuesを返す前にeraseToAnyPublisher()を使ってAnyPublisherにしているので、リターンは`AsyncPublisher<AnyPublisher<String, Never>>`となります。

呼び出し側はfor await内でStateやBindingに値を設定することができます。

```swift
import SwiftUI
import MapKit
import Combine

struct MapView: View {
    @State var locationText: String = ""
    @StateObject private var model = MyModel()
    
    var body: some View {
        VStack {
            Text(locationText)
            Map(coordinateRegion: $model.coordinateRegion)
        }
        .task {
            for await value in model.placemarkName() {
                locationText = value
            }
        }
    }
}

final class MyModel: ObservableObject {
    @Published var coordinateRegion = MKCoordinateRegion(center: .init(latitude: 35.681382,
                                                                       longitude: 139.740993),
                                                         span: .init(latitudeDelta: 0.01,
                                                                     longitudeDelta: 0.01))
    func placemarkName() -> AsyncPublisher<AnyPublisher<String, Never>> {
        $coordinateRegion
            .map { CLLocation(latitude: $0.center.latitude, longitude: $0.center.longitude) }
            .debounce(for: 0.5, scheduler: DispatchQueue.main)
            .flatMap { location in
                Future { promise in
                    Task {
                        do {
                            let placemarks = try await CLGeocoder().reverseGeocodeLocation(location)
                            promise(.success(placemarks.first?.name))
                        } catch {
                            promise(.success(nil))
                        }
                    }
                }
            }
            .compactMap { $0 }
            .eraseToAnyPublisher()
            .values
    }
}
```

![地図の位置から地名を表示する](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687960/23234366-d4d5-34c6-1574-f45d16f91b31.gif)