// To reproduce: // - Add NSLocationWhenInUseUsageDescription, NSLocationAlwaysAndWhenInUseUsageDescription and background mode location to project // - Launch app and accept permission prompts. If always permission prompt doesn't appear, tap "Prompt" // - Tap "Set Monitor" // - Close app // - Change device location e.g. using Features / Location / Custom Location (Set 1, 1) // - Re-open app and observe log messages import SwiftUI import CoreLocation class LocationMonitor { static let shared = LocationMonitor() private var monitor: CLMonitor? private var serviceSession: CLServiceSession? private init() { startServiceSession() startMonitoring() } private func startServiceSession() { let session = CLServiceSession(authorization: .always) self.serviceSession = session Task { for try await diagnostic in session.diagnostics { LogManager.shared.log("\(diagnostic)") } } } private func startMonitoring() { Task { let newMonitor = await CLMonitor("LocationMonitor") LogManager.shared.log("Started monitor") self.monitor = newMonitor for try await event in await newMonitor.events { let message = """ app state: \(UIApplication.shared.applicationState) event: \(event) """ LogManager.shared.log(message) } } } func setMonitor() async { guard let monitor else { fatalError("Monitor not available") } for identifier in await monitor.identifiers { await monitor.remove(identifier) } let center = CLLocationCoordinate2D(latitude: 1, longitude: 1) let condition = CLMonitor.CircularGeographicCondition(center: center, radius: 1000) await monitor.add(condition, identifier: UUID().uuidString) LogManager.shared.log("Set monitor") } } class AppDelegate: NSObject, UIApplicationDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil ) -> Bool { LogManager.shared.log("App launched with: \(launchOptions?.debugDescription ?? "none")") _ = LocationMonitor.shared return true } } @main struct CLMonitor_TestApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate let logManager = LogManager.shared var body: some Scene { WindowGroup { VStack { List(Array(logManager.entries.enumerated()), id: \.offset) { _, entry in Text(entry) .textSelection(.enabled) } HStack { Button("Prompt") { CLLocationManager().requestAlwaysAuthorization() } Button("Set Monitor") { Task { await LocationMonitor.shared.setMonitor() } } Button("Clear Log", role: .destructive) { LogManager.shared.clear() } } .buttonStyle(.borderedProminent) .padding() } } } } @MainActor @Observable class LogManager { static let shared = LogManager() private(set) var entries: [String] private init() { entries = UserDefaults.standard.array(forKey: "log") as? [String] ?? [] } func log(_ message: String) { let entry = "\(Date.now.ISO8601Format(.iso8601WithTimeZone(includingFractionalSeconds: true)))\n\(message)" entries.append(entry) UserDefaults.standard.set(entries, forKey: "log") } func clear() { entries.removeAll() UserDefaults.standard.removeObject(forKey: "log") } } extension CLMonitor.Event.State: @retroactive CustomDebugStringConvertible { public var debugDescription: String { switch self { case .satisfied: "satisfied" case .unknown: "unknown" case .unmonitored: "unmonitored" case .unsatisfied: "unsatisfied" @unknown default: "unknownState(\(rawValue))" } } } extension CLMonitor.Event: @retroactive CustomDebugStringConvertible { public var debugDescription: String { """ CLMonitor.Event(\ accuracyLimited: \(accuracyLimited), \ authorizationDenied: \(authorizationDenied), \ authorizationDeniedGlobally: \(authorizationDeniedGlobally), \ authorizationRequestInProgress: \(authorizationRequestInProgress), \ authorizationRestricted: \(authorizationRestricted), \ conditionLimitExceeded: \(conditionLimitExceeded), \ conditionUnsupported: \(conditionUnsupported), \ date: \(date.ISO8601Format(.iso8601WithTimeZone(includingFractionalSeconds: true))), \ identifier: \(identifier), \ insufficientlyInUse: \(insufficientlyInUse), \ persistenceUnavailable: \(persistenceUnavailable), \ refinement: \(refinement.debugDescription), \ serviceSessionRequired: \(serviceSessionRequired), \ state: \(state)\ ) """ } } extension CLServiceSession.Diagnostic: @retroactive CustomDebugStringConvertible { public var debugDescription: String { """ CLServiceSession.Diagnostic(\ alwaysAuthorizationDenied: \(alwaysAuthorizationDenied), \ authorizationDenied: \(authorizationDenied), \ authorizationDeniedGlobally: \(authorizationDeniedGlobally), \ authorizationRequestInProgress: \(authorizationRequestInProgress), \ authorizationRestricted: \(authorizationRestricted), \ fullAccuracyDenied: \(fullAccuracyDenied), \ insufficientlyInUse: \(insufficientlyInUse), \ serviceSessionRequired: \(serviceSessionRequired)\ ) """ } } extension UIApplication.State: @retroactive CustomDebugStringConvertible { public var debugDescription: String { switch self { case .active: "active" case .background: "background" case .inactive: "inactive" @unknown default: "unknownState(\(rawValue))" } } }