@tokamakdev/plugin-location 0.1.0-beta.128 → 0.1.0-beta.129

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,15 +18,15 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
18
18
  CLLocationManagerDelegate
19
19
  {
20
20
  let id = "location"
21
+ let host: TokamakHost
21
22
 
22
23
  /// A `requestPermission` waiting for the user to answer a prompt.
23
24
  private struct Prompt {
24
25
  /// The purpose key of the temporary precise location to ask for next.
25
26
  let purpose: String?
26
- let reply: TokamakPluginReply
27
+ let answer: CheckedContinuation<LocationPermission, Error>
27
28
  }
28
29
 
29
- private let host: TokamakHost
30
30
  private let defaults = UserDefaults.standard
31
31
  /// Reports the app's authorization, and asks for permission.
32
32
  private let authorization = CLLocationManager()
@@ -55,7 +55,7 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
55
55
  authorization.delegate = self
56
56
  monitoring.delegate = self
57
57
  if let stored = defaults.data(forKey: setupKey),
58
- let config = try? JSONSerialization.jsonObject(with: stored) as? [String: Any]
58
+ let config = try? JSONDecoder().decode(SetupConfig.self, from: stored)
59
59
  {
60
60
  apply(config)
61
61
  }
@@ -70,100 +70,148 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
70
70
  updates.resume()
71
71
  }
72
72
 
73
- func call(
74
- method: String,
75
- arguments: Any,
76
- reply: @escaping TokamakPluginReply
77
- ) -> (() -> Void)? {
78
- let options = arguments as? [String: Any] ?? [:]
79
- do throws(TokamakPluginError) {
80
- switch method {
81
- case "setup":
82
- if options["significantChanges"] as? Bool == true
83
- && !CLLocationManager.significantLocationChangeMonitoringAvailable()
84
- {
85
- throw .notSupported("The device cannot monitor significant changes of location")
86
- }
87
- apply(options)
88
- defaults.set(try? JSONSerialization.data(withJSONObject: options), forKey: setupKey)
89
- reply(.success(nil))
90
- case "recentPosition":
91
- reply(.success(recentPosition(maximumAge: (options["maximumAge"] as? Double ?? 0) / 1000)))
92
- case "currentPosition":
93
- let fix = TokamakLocationPositions(reply: reply)
94
- fix.locate()
95
- // The cancellation owns the location manager until the call ends.
96
- return fix.stop
97
- case "permission":
98
- reply(.success(permission()))
99
- case "requestPermission":
100
- try requestPermission(options, reply)
101
- case "servicesEnabled":
102
- DispatchQueue.global(qos: .userInitiated).async {
103
- reply(.success(CLLocationManager.locationServicesEnabled()))
104
- }
105
- case "openSettings":
106
- try host.requireUI()
107
- openSettings(reply)
108
- case "startUpdates":
109
- // A new background activity session starts only on screen.
110
- try host.requireUI()
111
- try requirePermission()
112
- try updates.start(options)
113
- reply(.success(nil))
114
- case "stopUpdates":
115
- updates.stop()
116
- reply(.success(nil))
117
- case "getUpdates":
118
- reply(.success(updates.options))
119
- case "addRegion":
120
- try requirePermission()
121
- guard authorization.accuracyAuthorization == .fullAccuracy else {
122
- throw TokamakPluginError(
123
- name: "NotAllowedError", message: "Regions need precise location")
124
- }
125
- Task { @MainActor in
126
- do throws(TokamakPluginError) {
127
- try await regions.add(options)
128
- reply(.success(nil))
129
- } catch {
130
- reply(.failure(error))
131
- }
132
- }
133
- case "getRegions":
134
- Task { @MainActor in reply(.success(await regions.all())) }
135
- case "removeRegion":
136
- guard let id = options["id"] as? String else {
137
- throw TokamakPluginError(name: "TypeError", message: "id must be a string")
138
- }
139
- Task { @MainActor in
140
- await regions.remove(id)
141
- reply(.success(nil))
142
- }
143
- default:
144
- throw .notSupported("\(id).\(method) is not supported")
73
+ /// Applies long-term location awareness, which the plugin applies again at each start.
74
+ func setup(_ config: SetupConfig) throws(TokamakPluginError) {
75
+ if config.significantChanges == true
76
+ && !CLLocationManager.significantLocationChangeMonitoringAvailable()
77
+ {
78
+ throw .notSupported("The device cannot monitor significant changes of location")
79
+ }
80
+ apply(config)
81
+ defaults.set(try? JSONEncoder().encode(config), forKey: setupKey)
82
+ }
83
+
84
+ /// Core Location's latest position when it is no older than `maximumAge`
85
+ /// milliseconds, or nil.
86
+ func recentPosition(maximumAge: Double) -> Position? {
87
+ guard let recent = authorization.location,
88
+ -recent.timestamp.timeIntervalSinceNow <= maximumAge / 1000
89
+ else { return nil }
90
+ return Position(recent)
91
+ }
92
+
93
+ /// A new fix, from a location manager that the call owns until it ends.
94
+ @MainActor
95
+ func currentPosition() async throws -> Position {
96
+ for try await position in TokamakLocationPositions.stream(watching: false) {
97
+ return position
98
+ }
99
+ throw CancellationError()
100
+ }
101
+
102
+ func watchPosition() -> AsyncThrowingStream<Position, Error> {
103
+ TokamakLocationPositions.stream(watching: true)
104
+ }
105
+
106
+ func permission() -> LocationPermission {
107
+ let status = authorization.authorizationStatus
108
+ return LocationPermission(
109
+ inUse: Self.state(status),
110
+ background: backgroundPermission(status),
111
+ precision: authorization.accuracyAuthorization == .reducedAccuracy ? .approximate : .precise
112
+ )
113
+ }
114
+
115
+ /// Asks for in-use permission while the user has not answered, or for
116
+ /// Always permission once from When In Use, then for temporary precise
117
+ /// location when `request` names a purpose.
118
+ @MainActor
119
+ func requestPermission(_ request: PermissionRequest?) async throws -> LocationPermission {
120
+ let always = request?.background == true
121
+ if always && !Self.declaresAlways {
122
+ throw TokamakPluginError.notSupported(
123
+ "Background location needs the location plugin's background build setting")
124
+ }
125
+ let purpose = try Self.purpose(request)
126
+ let status = authorization.authorizationStatus
127
+ guard status == .notDetermined || always && canUpgradeToAlways(status) else {
128
+ return try await respond(purpose: purpose)
129
+ }
130
+ try host.requireUI()
131
+ return try await withCheckedThrowingContinuation { answer in
132
+ prompts[UUID()] = Prompt(purpose: purpose, answer: answer)
133
+ if always {
134
+ defaults.set(true, forKey: askedForAlwaysKey)
135
+ authorization.requestAlwaysAuthorization()
136
+ } else {
137
+ authorization.requestWhenInUseAuthorization()
145
138
  }
146
- } catch {
147
- reply(.failure(error))
148
139
  }
149
- return nil
150
- }
151
-
152
- func subscribe(
153
- method: String,
154
- arguments: Any,
155
- reply: @escaping TokamakPluginReply
156
- ) throws(TokamakPluginError) -> (() -> Void) {
157
- if method == "watchPosition" {
158
- let watch = TokamakLocationPositions(reply: reply)
159
- watch.watch()
160
- // The release owns the location manager until the subscription ends.
161
- return watch.stop
140
+ }
141
+
142
+ /// Whether location services are on, which Core Location reports off the
143
+ /// main thread.
144
+ func servicesEnabled() async -> Bool {
145
+ CLLocationManager.locationServicesEnabled()
146
+ }
147
+
148
+ /// Opens the app's page of the system settings on iOS, and Location Services on macOS.
149
+ @MainActor
150
+ func openSettings() async throws {
151
+ try host.requireUI()
152
+ #if os(iOS)
153
+ let opened = await UIApplication.shared.open(
154
+ URL(string: UIApplication.openSettingsURLString)!)
155
+ #else
156
+ let page = "com.apple.preference.security?Privacy_LocationServices"
157
+ let opened =
158
+ URL(string: "x-apple.systempreferences:\(page)").map(NSWorkspace.shared.open) == true
159
+ #endif
160
+ guard opened else {
161
+ throw TokamakPluginError(
162
+ name: "OperationError", message: "The location settings could not be opened")
162
163
  }
163
- guard let remove = events.listen(method, reply) else {
164
- throw .notSupported("\(id).\(method) is not supported")
164
+ }
165
+
166
+ /// Starts continuous updates, which a new background activity session
167
+ /// allows only on screen.
168
+ func startUpdates(_ options: UpdateOptions?) throws(TokamakPluginError) {
169
+ try host.requireUI()
170
+ try requirePermission()
171
+ try updates.start(options ?? UpdateOptions(activity: nil))
172
+ }
173
+
174
+ func stopUpdates() {
175
+ updates.stop()
176
+ }
177
+
178
+ func getUpdates() -> UpdateOptions? {
179
+ updates.options
180
+ }
181
+
182
+ @MainActor
183
+ func addRegion(_ region: Region) async throws {
184
+ try requirePermission()
185
+ guard authorization.accuracyAuthorization == .fullAccuracy else {
186
+ throw TokamakPluginError(name: "NotAllowedError", message: "Regions need precise location")
165
187
  }
166
- return remove
188
+ try await regions.add(region)
189
+ }
190
+
191
+ @MainActor
192
+ func getRegions() async -> [MonitoredRegion] {
193
+ await regions.all()
194
+ }
195
+
196
+ @MainActor
197
+ func removeRegion(_ id: String) async {
198
+ await regions.remove(id)
199
+ }
200
+
201
+ func onLocationUpdate() -> AsyncStream<LocationUpdate> {
202
+ events.updates.stream()
203
+ }
204
+
205
+ func onRegionTransition() -> AsyncStream<RegionTransition> {
206
+ events.transitions.stream()
207
+ }
208
+
209
+ func onVisit() -> AsyncStream<Visit> {
210
+ events.visits.stream()
211
+ }
212
+
213
+ func onLocationError() -> AsyncStream<LocationErrorEvent> {
214
+ events.errors.stream()
167
215
  }
168
216
 
169
217
  /// Also called when each manager is created. Every manager reports the
@@ -204,45 +252,26 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
204
252
  }
205
253
  }
206
254
 
207
- /// Core Location's latest position when it is no older than `maximumAge`
208
- /// seconds, or nil.
209
- private func recentPosition(maximumAge: TimeInterval) -> Any? {
210
- guard let recent = authorization.location, -recent.timestamp.timeIntervalSinceNow <= maximumAge
211
- else { return nil }
212
- return TokamakLocationEvents.position(recent)
213
- }
214
-
215
255
  /// Starts the significant changes and visits `config` names, and stops the
216
256
  /// others. After a launch for one of their events, starting them again
217
257
  /// delivers it.
218
- private func apply(_ config: [String: Any]) {
219
- if config["significantChanges"] as? Bool == true {
258
+ private func apply(_ config: SetupConfig) {
259
+ if config.significantChanges == true {
220
260
  monitoring.startMonitoringSignificantLocationChanges()
221
261
  } else {
222
262
  monitoring.stopMonitoringSignificantLocationChanges()
223
263
  }
224
- if config["visits"] as? Bool == true {
264
+ if config.visits == true {
225
265
  monitoring.startMonitoringVisits()
226
266
  } else {
227
267
  monitoring.stopMonitoringVisits()
228
268
  }
229
269
  }
230
270
 
231
- /// The app's in-use and background permission, and the precision it gets.
232
- private func permission() -> [String: Any] {
233
- let status = authorization.authorizationStatus
234
- return [
235
- "inUse": Self.state(status),
236
- "background": backgroundPermission(status),
237
- "precision": authorization.accuracyAuthorization == .reducedAccuracy
238
- ? "approximate" : "precise",
239
- ]
240
- }
241
-
242
- private func backgroundPermission(_ status: CLAuthorizationStatus) -> String {
243
- if status == .authorizedAlways { return "granted" }
271
+ private func backgroundPermission(_ status: CLAuthorizationStatus) -> Permission {
272
+ if status == .authorizedAlways { return .granted }
244
273
  let asks = status == .notDetermined ? Self.declaresAlways : canUpgradeToAlways(status)
245
- return asks ? "prompt" : "denied"
274
+ return asks ? .prompt : .denied
246
275
  }
247
276
 
248
277
  /// Whether asking for Always shows a prompt: once, from When In Use, on iOS.
@@ -279,11 +308,11 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
279
308
  #endif
280
309
  }
281
310
 
282
- private static func state(_ status: CLAuthorizationStatus) -> String {
311
+ private static func state(_ status: CLAuthorizationStatus) -> Permission {
283
312
  switch status {
284
- case .notDetermined: "prompt"
285
- case .denied, .restricted: "denied"
286
- default: "granted"
313
+ case .notDetermined: .prompt
314
+ case .denied, .restricted: .denied
315
+ default: .granted
287
316
  }
288
317
  }
289
318
 
@@ -296,41 +325,13 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
296
325
  defaults.bool(forKey: askedForAlwaysKey)
297
326
  }
298
327
 
299
- /// Asks for in-use permission while the user has not answered, or for
300
- /// Always permission once from When In Use, then for temporary precise
301
- /// location when `options` name a purpose.
302
- private func requestPermission(
303
- _ options: [String: Any], _ reply: @escaping TokamakPluginReply
304
- ) throws(TokamakPluginError) {
305
- let always = options["background"] as? Bool == true
306
- if always && !Self.declaresAlways {
307
- throw .notSupported(
308
- "Background location needs the location plugin's background build setting")
309
- }
310
- let prompt = Prompt(purpose: try Self.purpose(options), reply: reply)
311
- let status = authorization.authorizationStatus
312
- let upgrades = always && canUpgradeToAlways(status)
313
- guard status == .notDetermined || upgrades else {
314
- respond(to: prompt)
315
- return
316
- }
317
- try host.requireUI()
318
- prompts[UUID()] = prompt
319
- if always {
320
- defaults.set(true, forKey: askedForAlwaysKey)
321
- authorization.requestAlwaysAuthorization()
322
- } else {
323
- authorization.requestWhenInUseAuthorization()
324
- }
325
- }
326
-
327
- /// The purpose key of the temporary precise location `options` ask for.
328
- private static func purpose(_ options: [String: Any]) throws(TokamakPluginError) -> String? {
329
- guard options["precision"] as? String == "precise" else { return nil }
328
+ /// The purpose key of the temporary precise location `request` asks for.
329
+ private static func purpose(_ request: PermissionRequest?) throws(TokamakPluginError) -> String? {
330
+ guard request?.precision == .precise else { return nil }
330
331
  let purposes =
331
332
  Bundle.main.object(forInfoDictionaryKey: "NSLocationTemporaryUsageDescriptionDictionary")
332
333
  as? [String: Any]
333
- guard let purpose = options["purpose"] as? String, purposes?[purpose] != nil else {
334
+ guard let purpose = request?.purpose, purposes?[purpose] != nil else {
334
335
  throw TokamakPluginError(
335
336
  name: "TypeError",
336
337
  message: "precise location needs a purpose from the accuracy-purposes build setting")
@@ -358,44 +359,29 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
358
359
  /// Answers the prompts `keys` that still wait.
359
360
  private func answer(_ keys: [UUID]) {
360
361
  for key in keys {
361
- if let prompt = prompts.removeValue(forKey: key) {
362
- respond(to: prompt)
363
- }
362
+ guard let prompt = prompts.removeValue(forKey: key) else { continue }
363
+ Task { @MainActor in await answer(prompt) }
364
364
  }
365
365
  }
366
366
 
367
- /// Replies to `prompt` with the app's permission, first asking for temporary
368
- /// precise location for its purpose when the app has approximate location.
369
- private func respond(to prompt: Prompt) {
370
- guard let purpose = prompt.purpose, hasPermission,
371
- authorization.accuracyAuthorization == .reducedAccuracy
372
- else {
373
- prompt.reply(.success(permission()))
374
- return
375
- }
376
- do throws(TokamakPluginError) {
377
- try host.requireUI()
367
+ @MainActor
368
+ private func answer(_ prompt: Prompt) async {
369
+ do {
370
+ prompt.answer.resume(returning: try await respond(purpose: prompt.purpose))
378
371
  } catch {
379
- prompt.reply(.failure(error))
380
- return
381
- }
382
- authorization.requestTemporaryFullAccuracyAuthorization(withPurposeKey: purpose) { _ in
383
- DispatchQueue.main.async { prompt.reply(.success(self.permission())) }
372
+ prompt.answer.resume(throwing: error)
384
373
  }
385
374
  }
386
375
 
387
- /// Opens the app's page of the system settings on iOS, and Location Services on macOS.
388
- private func openSettings(_ reply: @escaping TokamakPluginReply) {
389
- let notOpened = TokamakPluginError(
390
- name: "OperationError", message: "The location settings could not be opened")
391
- #if os(iOS)
392
- UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) { opened in
393
- reply(opened ? .success(nil) : .failure(notOpened))
394
- }
395
- #else
396
- let page = "com.apple.preference.security?Privacy_LocationServices"
397
- let opened = URL(string: "x-apple.systempreferences:\(page)").map(NSWorkspace.shared.open)
398
- reply(opened == true ? .success(nil) : .failure(notOpened))
399
- #endif
376
+ /// The app's permission, after asking for temporary precise location for
377
+ /// `purpose` when the app has approximate location.
378
+ @MainActor
379
+ private func respond(purpose: String?) async throws -> LocationPermission {
380
+ if let purpose, hasPermission, authorization.accuracyAuthorization == .reducedAccuracy {
381
+ try host.requireUI()
382
+ _ = try? await authorization.requestTemporaryFullAccuracyAuthorization(
383
+ withPurposeKey: purpose)
384
+ }
385
+ return permission()
400
386
  }
401
387
  }
@@ -1,38 +1,48 @@
1
1
  import CoreLocation
2
2
  import Foundation
3
3
 
4
- /// Positions from a location manager of its own, for one `getCurrentPosition`
5
- /// or `watchPosition`. Use it on the main thread.
6
- final class TokamakLocationPositions: NSObject, CLLocationManagerDelegate {
4
+ /// Positions from a location manager of its own, for one `currentPosition`
5
+ /// or `watchPosition`. Use it on the main thread, where the end of its stream
6
+ /// stops it.
7
+ final class TokamakLocationPositions: NSObject, CLLocationManagerDelegate, @unchecked Sendable {
7
8
  private let manager = CLLocationManager()
8
- private let reply: TokamakPluginReply
9
- private var watching = false
9
+ private let continuation: AsyncThrowingStream<TokamakLocationPlugin.Position, Error>.Continuation
10
+ private let watching: Bool
10
11
 
11
- /// Passes `reply` the new fix, or each position as the device moves, or a
12
- /// failure.
13
- init(reply: @escaping TokamakPluginReply) {
14
- self.reply = reply
12
+ /// A new fix, or each position as the device moves when `watching`, until
13
+ /// the stream ends.
14
+ static func stream(watching: Bool) -> AsyncThrowingStream<TokamakLocationPlugin.Position, Error> {
15
+ AsyncThrowingStream { continuation in
16
+ let positions = TokamakLocationPositions(continuation, watching: watching)
17
+ // The stream owns the location manager until it ends.
18
+ continuation.onTermination = { _ in
19
+ DispatchQueue.main.async { positions.manager.stopUpdatingLocation() }
20
+ }
21
+ if watching {
22
+ positions.manager.startUpdatingLocation()
23
+ } else {
24
+ positions.manager.requestLocation()
25
+ }
26
+ }
27
+ }
28
+
29
+ private init(
30
+ _ continuation: AsyncThrowingStream<TokamakLocationPlugin.Position, Error>.Continuation,
31
+ watching: Bool
32
+ ) {
33
+ self.continuation = continuation
34
+ self.watching = watching
15
35
  super.init()
16
36
  manager.delegate = self
17
37
  manager.desiredAccuracy = kCLLocationAccuracyBest
18
38
  }
19
39
 
20
- func locate() {
21
- manager.requestLocation()
22
- }
23
-
24
- func watch() {
25
- watching = true
26
- manager.startUpdatingLocation()
27
- }
28
-
29
- func stop() {
30
- manager.stopUpdatingLocation()
31
- }
32
-
33
40
  func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
34
41
  for location in watching ? locations : locations.suffix(1) {
35
- reply(.success(TokamakLocationEvents.position(location)))
42
+ continuation.yield(TokamakLocationPlugin.Position(location))
43
+ }
44
+ if !watching {
45
+ continuation.finish()
36
46
  }
37
47
  }
38
48
 
@@ -40,7 +50,7 @@ final class TokamakLocationPositions: NSObject, CLLocationManagerDelegate {
40
50
  /// position yet.
41
51
  func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
42
52
  guard (error as NSError).code == CLError.Code.locationUnknown.rawValue else {
43
- return reply(.failure(.location(error)))
53
+ return continuation.finish(throwing: TokamakPluginError.location(error))
44
54
  }
45
55
  if !watching {
46
56
  manager.requestLocation()
@@ -5,7 +5,7 @@ import Foundation
5
5
  import UIKit
6
6
  #endif
7
7
 
8
- /// The JSON of each monitored region, as `addRegion` received it, by id.
8
+ /// The JSON of each monitored region, by id.
9
9
  private let regionsKey = "tokamak.location.regions"
10
10
  /// The ids of the regions whose last transition was an entry.
11
11
  private let insideKey = "tokamak.location.inside"
@@ -30,26 +30,23 @@ final class TokamakLocationRegions {
30
30
  return await CLMonitor(monitorName)
31
31
  }
32
32
  self.monitor = monitor
33
- Task { @MainActor [weak self] in
34
- do {
35
- for try await event in await monitor.value.events {
36
- self?.receive(event)
37
- }
38
- } catch {
39
- self?.events.fail(
40
- .locationUnavailable(error.localizedDescription))
33
+ events.task { [weak self] in
34
+ for try await event in await monitor.value.events {
35
+ self?.receive(event)
41
36
  }
42
37
  }
43
38
  }
44
39
 
45
40
  /// The regions, by id.
46
- private var regions: [String: [String: Any]] {
41
+ private var regions: [String: TokamakLocationPlugin.Region] {
47
42
  get {
48
43
  defaults.data(forKey: regionsKey)
49
- .flatMap { try? JSONSerialization.jsonObject(with: $0) as? [String: [String: Any]] } ?? [:]
44
+ .flatMap {
45
+ try? JSONDecoder().decode([String: TokamakLocationPlugin.Region].self, from: $0)
46
+ } ?? [:]
50
47
  }
51
48
  set {
52
- defaults.set(try? JSONSerialization.data(withJSONObject: newValue), forKey: regionsKey)
49
+ defaults.set(try? JSONEncoder().encode(newValue), forKey: regionsKey)
53
50
  }
54
51
  }
55
52
 
@@ -58,17 +55,12 @@ final class TokamakLocationRegions {
58
55
  set { defaults.set(Array(newValue), forKey: insideKey) }
59
56
  }
60
57
 
61
- /// Monitors the region `region` describes, replacing one with the same id.
62
- /// It starts outside, so a device already inside reports an entry, as on
63
- /// Android. `CLMonitor` keeps the state of a condition added again under its
64
- /// identifier, so the old condition is removed first.
65
- func add(_ region: [String: Any]) async throws(TokamakPluginError) {
66
- guard let id = region["id"] as? String, let radius = region["radius"] as? Double,
67
- let latitude = region["latitude"] as? Double, let longitude = region["longitude"] as? Double
68
- else {
69
- throw TokamakPluginError(
70
- name: "TypeError", message: "region must have an id, latitude, longitude and radius")
71
- }
58
+ /// Monitors `region`, replacing one with the same id. It starts outside, so
59
+ /// a device already inside reports an entry, as on Android. `CLMonitor`
60
+ /// keeps the state of a condition added again under its identifier, so the
61
+ /// old condition is removed first.
62
+ func add(_ region: TokamakLocationPlugin.Region) async throws(TokamakPluginError) {
63
+ let id = region.id
72
64
  let monitor = await monitor.value
73
65
  let monitored = await monitor.identifiers
74
66
  guard monitored.count < regionLimit || monitored.contains(id) else {
@@ -76,8 +68,8 @@ final class TokamakLocationRegions {
76
68
  name: "QuotaExceededError",
77
69
  message: "The app monitors \(regionLimit) regions, the most the system allows")
78
70
  }
79
- let center = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
80
- let condition = CLMonitor.CircularGeographicCondition(center: center, radius: radius)
71
+ let center = CLLocationCoordinate2D(latitude: region.latitude, longitude: region.longitude)
72
+ let condition = CLMonitor.CircularGeographicCondition(center: center, radius: region.radius)
81
73
  regions[id] = region
82
74
  inside.remove(id)
83
75
  await monitor.remove(id)
@@ -92,19 +84,22 @@ final class TokamakLocationRegions {
92
84
  }
93
85
 
94
86
  /// The monitored regions, each with whether the device is inside it.
95
- func all() async -> [[String: Any]] {
87
+ func all() async -> [TokamakLocationPlugin.MonitoredRegion] {
96
88
  let monitor = await monitor.value
97
89
  let regions = regions
98
- var all: [[String: Any]] = []
90
+ var all: [TokamakLocationPlugin.MonitoredRegion] = []
99
91
  for id in await monitor.identifiers {
100
- guard var region = regions[id] else { continue }
101
- region["state"] =
92
+ guard let region = regions[id] else { continue }
93
+ let state: TokamakLocationPlugin.RegionState =
102
94
  switch await monitor.record(for: id)?.lastEvent.state {
103
- case .satisfied: "inside"
104
- case .unsatisfied: "outside"
105
- default: "unknown"
95
+ case .satisfied: .inside
96
+ case .unsatisfied: .outside
97
+ default: .unknown
106
98
  }
107
- all.append(region)
99
+ all.append(
100
+ TokamakLocationPlugin.MonitoredRegion(
101
+ id: id, latitude: region.latitude, longitude: region.longitude, radius: region.radius,
102
+ onEntry: region.onEntry ?? true, onExit: region.onExit ?? true, state: state))
108
103
  }
109
104
  return all
110
105
  }
@@ -121,7 +116,7 @@ final class TokamakLocationRegions {
121
116
  let entered = event.state == .satisfied
122
117
  guard entered != inside.contains(id) else { return }
123
118
  inside = entered ? inside.union([id]) : inside.subtracting([id])
124
- if region[entered ? "onEntry" : "onExit"] as? Bool != false {
119
+ if (entered ? region.onEntry : region.onExit) != false {
125
120
  events.transition(id, entered: entered, at: event.date)
126
121
  }
127
122
  }