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に設定して偶数を表示したりできます。
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
を使います。
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に値を設定することができます。
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
}
}