Open1
Swift: delegateで結果が返ってくるパターンをasync/awaitでラップする
何かを実行すると、delegate呼び出しで結果が返ってくる、というようなパターンがあります。
Swift Concurrencyの例題には、CompletionHandlerのようなパターンはよく載っているのですが、delegateのパターンはあまり見かけません。
ちょっと試してみたので未来の自分に向けて覚え書きしておきます。
今回試したのは、CLLocationManager
+CLLocationManagerDelegate
で、現在地を取得するパターンです。
CLLocationManager.startUpdatingLocation()
を呼ぶと、CLLocationManagerDelegate .locationManager(_:didUpdateLocations:)
で現在地のCLLocation
の配列が返ってきます。
(返ってくるというか、stopUpdatingLocation()
を呼ぶまでdelegateが繰り返し呼ばれます)
考え方としては、delegateの呼び出しの中からCompletionHandlerを呼び出すようにして、CompletionHandler内でwithCheckedContinuation.resume
を呼ぶ、というような感じになります。
CurrentLocationManager.swift
import CoreLocation
class CurrentLocationManager: NSObject, CLLocationManagerDelegate {
private let manager = CLLocationManager()
private var completionHandler: ((CLLocationCoordinate2D) -> (Void))? = nil
private static var s_locationManager: CurrentLocationManager? = nil
var location: CLLocationCoordinate2D = CLLocationCoordinate2D()
@MainActor
static func shared() -> CurrentLocationManager {
if let manager = s_locationManager {
return manager
}
s_locationManager = CurrentLocationManager()
return s_locationManager!
}
private override init() {
super.init()
manager.delegate = self
manager.requestWhenInUseAuthorization()
manager.desiredAccuracy = kCLLocationAccuracyBest
manager.distanceFilter = 2.0
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
DispatchQueue.main.async {
// 1回取れればよいので、すぐに座標更新を止める
manager.stopUpdatingLocation()
locations.last.map {
//渡ってきた座標を覚えておく
self.location = $0.coordinate
}
if let handler = self.completionHandler {
// stopUpdatingLocation()をしていても、handler呼び出し中にこのdelegate呼び出しが再度来る場合がある。
// handler内でwithCheckedContinuation.resumeを呼んでいるが、resumeは1回しか呼べず2回以上resumeを(=handlerを)呼ぶとエラーになる。
// このため、handler呼び出し前に次の呼び出しができないようにnilにしておく
self.completionHandler = nil
handler(self.location)
}
}
}
func startUpdatingLocation() {
manager.startUpdatingLocation()
}
static func getCurrentLocation() async -> CLLocationCoordinate2D {
let shared = await CurrentLocationManager.shared()
return await withCheckedContinuation { continuation in
// delegate呼び出しから呼ばれるCompletionHandler。
// CompletionHandler内でresumeする。
shared.completionHandler = { center in
continuation.resume(returning: center)
}
// CompletionHandlerをセットしたうえで、startする(と、しばらくしてdelegateが呼び出される)。
shared.startUpdatingLocation()
}
}
}
使い方は以下のような感じになります。
import SwiftUI
import CoreLocation
struct ContentView: View {
@State var coordinate: CLLocationCoordinate2D = CLLocationCoordinate2D()
var body: some View {
VStack {
Button(action: {
Task {
self.coordinate = await CurrentLocationManager.getCurrentLocation()
}
}, label: {
Text("現在地座標を取得")
})
.buttonStyle(.bordered)
Text("Location: \(coordinate.latitude), \(coordinate.longitude)")
}
.padding()
}
}