👻

デリゲートメソッドのSwift Concurrency対応

に公開

概要

SwiftUI アプリで位置情報を取得する際には、通常CLLocationManagerを使い、その結果は CLLocationManagerDelegateを通じて受け取ります。
しかし、デリゲートでのコールバックは複数に分散し、処理の流れが追いづらくなるという課題があります。

そこで本記事では、Swift Concurrencyのasync/awaitを用いて、デリゲートベースの非同期処理をより宣言的で読みやすい形に書き換える方法を紹介します。

背景:デリゲートの課題

Appleの多くのAPI(例: CLLocationManager)では、非同期の結果を以下のようなデリゲートメソッドで受け取ります:

このような構造には次のような問題があります:
•処理の流れが複数のメソッドに分かれ、追いづらい
•結果を保持するための中間変数が必要
•UI更新や状態管理のロジックが分散しやすい

こうした課題を、Swift Concurrencyのasync/awaitによって解消できます。

解決法:デリゲートをasync関数にラップする

今回は位置情報を一度だけ取得するrequestLocation()メソッドをasync対応に変換します。
この処理で実現したいのは、「非同期の結果(現在位置)を待って受け取りたい」なので、withCheckedThrowingContinuation(エラーが発生しない場合はwithCheckedContinuation)を使ってデリゲートをラップします。

実装ステップ

1.withCheckedThrowingContinuationを使って非同期の待機処理を作る
2.continuationを一時的にプロパティに保持
3.デリゲートメソッドで値を受け取ったらresume()を使って非同期処理を完了させる

実装コード

// Model
import SwiftUI
import CoreLocation

@Observable
final class LocationModel: NSObject {

    private let locationManager = CLLocationManager()

    var coordinate: CLLocationCoordinate2D?
    var errorMessage: String?

    @ObservationIgnored
    var locationContinuation: CheckedContinuation<CLLocationCoordinate2D, Error>?

    override init() {
        super.init()
        locationManager.delegate = self
        locationManager.requestWhenInUseAuthorization()
    }

    @MainActor
    func requestLocation() async {
        do {
            let result = try await withCheckedThrowingContinuation { continuation in
                locationContinuation = continuation
                locationManager.requestLocation()
            }
            coordinate = result
            errorMessage = nil
        } catch {
            coordinate = nil
            errorMessage = error.localizedDescription
        }
    }
}

extension LocationModel: CLLocationManagerDelegate {
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        guard let location = locations.first else { return }
        locationContinuation?.resume(returning: location.coordinate)
        locationContinuation = nil
    }

    func locationManager(_ manager: CLLocationManager, didFailWithError error: any Error) {
        locationContinuation?.resume(throwing: error)
        locationContinuation = nil
    }
}

// View
import SwiftUI

struct LocationView: View {
    @State private var locationModel: LocationModel = .init()

    var body: some View {
        VStack {
            if let data = locationModel.coordinate {
                Text("緯度:\(data.latitude)")
                Text("経度:\(data.longitude)")
            } else {
                Text("位置情報なし")
            }
        }
        .task {
            await locationModel.requestLocation()
        }
    }
}

#Preview {
    LocationView()
}

おわりに

このように、従来のデリゲートベースAPIでも Swift Concurrencyを活用すれば、より読みやすく保守しやすい設計が可能になります。
「非同期処理の見通しを良くしたい」「状態管理をシンプルにしたい」場面では、ぜひ withCheckedContinuationの活用を検討してみてください。

Discussion