LoginSignup
7
1

More than 1 year has passed since last update.

Swift CombineをAsyncSequenceとして処理する

Last updated at Posted at 2023-02-09

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

地図の位置から地名を表示する

7
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
1