@mmerterden/multi-agent-pipeline 20.0.0 → 20.2.0
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.
- package/CHANGELOG.md +55 -0
- package/README.md +5 -5
- package/README.tr.md +5 -5
- package/SECURITY.md +3 -3
- package/docs/adr/0011-dormant-ci.md +10 -1
- package/docs/architecture.md +2 -2
- package/docs/ecosystem.md +5 -5
- package/docs/facts.json +8 -7
- package/install/_codex-agents.mjs +1 -1
- package/manifest.json +92 -64
- package/package.json +1 -1
- package/pipeline/agents/code-reviewer.md +2 -2
- package/pipeline/agents/dev-critic.md +5 -5
- package/pipeline/agents/security-auditor.md +80 -72
- package/pipeline/commands/figma-to-swiftui.md +1 -1
- package/pipeline/commands/multi-agent/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/channels/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/diff-explain/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/help/SKILL.md +2 -0
- package/pipeline/commands/multi-agent/scan/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/security-review/SKILL.md +52 -0
- package/pipeline/commands/multi-agent/sync/SKILL.md +3 -3
- package/pipeline/multi-agent-refs/component-dispatch.md +5 -5
- package/pipeline/multi-agent-refs/cross-cli-contract.md +6 -6
- package/pipeline/multi-agent-refs/features/security-audit.md +55 -0
- package/pipeline/multi-agent-refs/phases/modes.md +1 -1
- package/pipeline/multi-agent-refs/phases/phase-3-review.md +9 -15
- package/pipeline/multi-agent-refs/phases/phase-5-report.md +1 -1
- package/pipeline/multi-agent-refs/threat-model.md +39 -0
- package/pipeline/schemas/agent-state.schema.json +23 -0
- package/pipeline/schemas/phases.json +1 -2
- package/pipeline/schemas/prefs.schema.json +0 -4
- package/pipeline/schemas/reviewer-output.schema.json +99 -2
- package/pipeline/schemas/security-finding.schema.json +144 -0
- package/pipeline/scripts/_stack-routing.mjs +1 -0
- package/pipeline/scripts/gc-abandoned.sh +16 -9
- package/pipeline/scripts/render-work-summary.sh +7 -4
- package/pipeline/skills/.skill-manifest.json +47 -23
- package/pipeline/skills/.skills-index.json +75 -9
- package/pipeline/skills/shared/README.md +13 -7
- package/pipeline/skills/shared/core/multi-agent/SKILL.md +3 -4
- package/pipeline/skills/shared/core/multi-agent-scan/SKILL.md +2 -2
- package/pipeline/skills/shared/core/multi-agent-security-review/SKILL.md +29 -0
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +3 -3
- package/pipeline/skills/shared/external/android-architecture/SKILL.md +71 -0
- package/pipeline/skills/shared/external/android-architecture/references/patterns.md +142 -0
- package/pipeline/skills/shared/external/android-build-quality-gates/SKILL.md +314 -0
- package/pipeline/skills/shared/external/android-build-quality-gates/references/patterns.md +432 -0
- package/pipeline/skills/shared/external/android-datastore/SKILL.md +236 -0
- package/pipeline/skills/shared/external/android-datastore/references/patterns.md +297 -0
- package/pipeline/skills/shared/external/android-design-tokens-codegen/SKILL.md +249 -0
- package/pipeline/skills/shared/external/android-design-tokens-codegen/references/patterns.md +270 -0
- package/pipeline/skills/shared/external/android-jetpack-compose-expert/SKILL.md +62 -0
- package/pipeline/skills/shared/external/android-mvi-viewmodel/SKILL.md +255 -0
- package/pipeline/skills/shared/external/android-mvi-viewmodel/references/patterns.md +257 -0
- package/pipeline/skills/shared/external/android-performance/SKILL.md +86 -602
- package/pipeline/skills/shared/external/android-performance/references/patterns.md +659 -0
- package/pipeline/skills/shared/external/android-security/SKILL.md +117 -430
- package/pipeline/skills/shared/external/android-security/references/patterns.md +690 -0
- package/pipeline/skills/shared/external/{android_ui_verification → android-ui-verification}/SKILL.md +1 -1
- package/pipeline/skills/shared/external/api-security-best-practices/SKILL.md +35 -733
- package/pipeline/skills/shared/external/api-security-best-practices/references/auth.md +299 -0
- package/pipeline/skills/shared/external/api-security-best-practices/references/input-validation.md +255 -0
- package/pipeline/skills/shared/external/api-security-best-practices/references/rate-limiting.md +167 -0
- package/pipeline/skills/shared/external/app-intents/SKILL.md +39 -174
- package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +178 -0
- package/pipeline/skills/shared/external/compose-components/SKILL.md +48 -0
- package/pipeline/skills/shared/external/compose-components/references/patterns.md +200 -0
- package/pipeline/skills/shared/external/compose-navigation/SKILL.md +66 -3
- package/pipeline/skills/shared/external/compose-navigation/references/patterns.md +191 -0
- package/pipeline/skills/shared/external/compose-testing/SKILL.md +107 -397
- package/pipeline/skills/shared/external/compose-testing/references/patterns.md +631 -0
- package/pipeline/skills/shared/external/gradle-kotlin-dsl/SKILL.md +121 -449
- package/pipeline/skills/shared/external/gradle-kotlin-dsl/references/patterns.md +715 -0
- package/pipeline/skills/shared/external/kotlin-coroutines-expert/SKILL.md +143 -0
- package/pipeline/skills/shared/external/mapkit-location/SKILL.md +27 -102
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +42 -0
- package/pipeline/skills/shared/external/retrofit-networking/SKILL.md +94 -383
- package/pipeline/skills/shared/external/retrofit-networking/references/patterns.md +640 -0
- package/pipeline/skills/shared/external/room-database/SKILL.md +101 -440
- package/pipeline/skills/shared/external/room-database/references/patterns.md +614 -0
- package/pipeline/skills/shared/external/security-review/SKILL.md +64 -0
- package/pipeline/skills/shared/external/security-review/references/owasp-mobile-top10-2024.md +53 -0
- package/pipeline/skills/shared/external/security-review/references/owasp-web-api-top10-2021.md +56 -0
- package/pipeline/skills/shared/external/storekit/SKILL.md +69 -343
- package/pipeline/skills/shared/external/storekit/references/core-patterns.md +371 -0
- package/pipeline/skills/shared/external/widgetkit/SKILL.md +25 -101
- package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +107 -0
- package/pipeline/skills/skills-index.md +8 -2
- package/pipeline/commands/security-review.md +0 -6
|
@@ -70,6 +70,78 @@ val searchResults: Flow<List<Item>> = searchQuery
|
|
|
70
70
|
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
+
### 4. Injectable Dispatchers
|
|
74
|
+
|
|
75
|
+
Never hardcode `Dispatchers.IO` / `Dispatchers.Default` inside repositories and data sources. Inject them through Hilt qualifiers so a test can swap in a `TestDispatcher` and control virtual time without touching production code.
|
|
76
|
+
|
|
77
|
+
```kotlin
|
|
78
|
+
@Retention(AnnotationRetention.BINARY)
|
|
79
|
+
@Qualifier
|
|
80
|
+
annotation class IoDispatcher
|
|
81
|
+
|
|
82
|
+
@Retention(AnnotationRetention.BINARY)
|
|
83
|
+
@Qualifier
|
|
84
|
+
annotation class DefaultDispatcher
|
|
85
|
+
|
|
86
|
+
@Module
|
|
87
|
+
@InstallIn(SingletonComponent::class)
|
|
88
|
+
object DispatchersModule {
|
|
89
|
+
@IoDispatcher
|
|
90
|
+
@Provides
|
|
91
|
+
fun provideIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
|
|
92
|
+
|
|
93
|
+
@DefaultDispatcher
|
|
94
|
+
@Provides
|
|
95
|
+
fun provideDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
class ItemRepository @Inject constructor(
|
|
99
|
+
private val api: ItemApi,
|
|
100
|
+
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
|
|
101
|
+
) {
|
|
102
|
+
suspend fun load(id: String): Item = withContext(ioDispatcher) {
|
|
103
|
+
api.fetch(id).toDomain()
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
In tests, provide a single `TestDispatcher` (typically `StandardTestDispatcher`) for both qualifiers so `runTest`'s scheduler drives everything.
|
|
109
|
+
|
|
110
|
+
The `DispatcherProvider` interface idiom (one injected object exposing `io`, `default`, `main`) is a valid alternative and helps when a class needs several dispatchers, but the per-dispatcher qualifier form is leaner: it names the exact dependency at the constructor and avoids a wrapper type.
|
|
111
|
+
|
|
112
|
+
### 5. Cancellation-Safe Error Handling
|
|
113
|
+
|
|
114
|
+
A broad `catch` swallows `CancellationException` and silently breaks structured concurrency: the coroutine keeps running after its scope was cancelled. Rethrow it explicitly before any `catch (Throwable)` or `catch (Exception)`.
|
|
115
|
+
|
|
116
|
+
```kotlin
|
|
117
|
+
suspend fun loadOrFallback(): Data =
|
|
118
|
+
try {
|
|
119
|
+
repo.fetch()
|
|
120
|
+
} catch (e: CancellationException) {
|
|
121
|
+
throw e
|
|
122
|
+
} catch (e: Throwable) {
|
|
123
|
+
Data.empty()
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Choose the strategy per branch. On the critical branch, let cancellation and real failures propagate so the caller sees them. On an optional parallel branch, degrade to a fallback after the rethrow.
|
|
128
|
+
|
|
129
|
+
```kotlin
|
|
130
|
+
suspend fun loadScreen(): ScreenData = coroutineScope {
|
|
131
|
+
val core = async { repo.loadCore() }
|
|
132
|
+
val banner = async {
|
|
133
|
+
try {
|
|
134
|
+
repo.loadPromoBanner()
|
|
135
|
+
} catch (e: CancellationException) {
|
|
136
|
+
throw e
|
|
137
|
+
} catch (e: Throwable) {
|
|
138
|
+
null
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
ScreenData(core = core.await(), banner = banner.await())
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
73
145
|
## Examples
|
|
74
146
|
|
|
75
147
|
### Example 1: Parallel Execution with Error Handling
|
|
@@ -87,6 +159,77 @@ suspend fun fetchDataWithErrorHandling() = supervisorScope {
|
|
|
87
159
|
}
|
|
88
160
|
```
|
|
89
161
|
|
|
162
|
+
### Example 2: Fire-and-Forget Background Persistence
|
|
163
|
+
|
|
164
|
+
A long-lived scope built on `SupervisorJob` for writes whose failure must not cancel siblings or crash the caller. The body is always guarded, because an uncaught throw in a supervisor child still reaches the scope's exception handler.
|
|
165
|
+
|
|
166
|
+
```kotlin
|
|
167
|
+
class CacheWriter @Inject constructor(
|
|
168
|
+
@IoDispatcher ioDispatcher: CoroutineDispatcher
|
|
169
|
+
) {
|
|
170
|
+
private val scope = CoroutineScope(SupervisorJob() + ioDispatcher)
|
|
171
|
+
|
|
172
|
+
fun persist(entry: CacheEntry) {
|
|
173
|
+
scope.launch {
|
|
174
|
+
try {
|
|
175
|
+
dao.upsert(entry)
|
|
176
|
+
} catch (e: CancellationException) {
|
|
177
|
+
throw e
|
|
178
|
+
} catch (e: Throwable) {
|
|
179
|
+
logger.warn("cache write failed", e)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Example 3: Double-Checked Lazy Async Init in a Suspend Context
|
|
187
|
+
|
|
188
|
+
Guard a one-time async initialization with `Mutex.withLock`, not `synchronized`. `synchronized` blocks the thread and cannot host suspend calls; `withLock` suspends the coroutine and releases cooperatively.
|
|
189
|
+
|
|
190
|
+
```kotlin
|
|
191
|
+
class TokenProvider @Inject constructor(private val api: AuthApi) {
|
|
192
|
+
private val mutex = Mutex()
|
|
193
|
+
@Volatile private var cached: Token? = null
|
|
194
|
+
|
|
195
|
+
suspend fun token(): Token {
|
|
196
|
+
cached?.let { return it }
|
|
197
|
+
return mutex.withLock {
|
|
198
|
+
cached ?: api.fetchToken().also { cached = it }
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Example 4: Awaiting a Suspend Result Inside a Synchronous Callback
|
|
205
|
+
|
|
206
|
+
A framework callback that must return a value synchronously cannot call a suspend function directly, and `runBlocking` on the main or a callback thread risks deadlock. Bridge with a scoped `launch`, a lock/condition, and `withTimeout` so a stalled producer cannot block forever.
|
|
207
|
+
|
|
208
|
+
```kotlin
|
|
209
|
+
fun interceptSync(request: Request): Response {
|
|
210
|
+
val lock = ReentrantLock()
|
|
211
|
+
val done = lock.newCondition()
|
|
212
|
+
var result: Response? = null
|
|
213
|
+
|
|
214
|
+
scope.launch {
|
|
215
|
+
val value = try {
|
|
216
|
+
withTimeout(5_000) { resolver.resolve(request) }
|
|
217
|
+
} catch (e: Throwable) {
|
|
218
|
+
Response.passthrough(request)
|
|
219
|
+
}
|
|
220
|
+
lock.withLock {
|
|
221
|
+
result = value
|
|
222
|
+
done.signalAll()
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
lock.withLock {
|
|
227
|
+
while (result == null) done.await()
|
|
228
|
+
return result!!
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
90
233
|
## Best Practices
|
|
91
234
|
|
|
92
235
|
- ✅ **Do:** Use `Dispatchers.IO` for blocking I/O operations.
|
|
@@ -130,29 +130,10 @@ Map {
|
|
|
130
130
|
### Camera Position
|
|
131
131
|
|
|
132
132
|
`MapCameraPosition` controls what the map displays. Bind it to let the user
|
|
133
|
-
interact and to programmatically move the camera.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
@State private var position: MapCameraPosition = .region(
|
|
138
|
-
MKCoordinateRegion(
|
|
139
|
-
center: CLLocationCoordinate2D(latitude: 37.334, longitude: -122.009),
|
|
140
|
-
span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
|
|
141
|
-
)
|
|
142
|
-
)
|
|
143
|
-
|
|
144
|
-
// Follow user location
|
|
145
|
-
@State private var position: MapCameraPosition = .userLocation(fallback: .automatic)
|
|
146
|
-
|
|
147
|
-
// Specific camera angle (3D perspective)
|
|
148
|
-
@State private var position: MapCameraPosition = .camera(
|
|
149
|
-
MapCamera(centerCoordinate: applePark, distance: 1000, heading: 90, pitch: 60)
|
|
150
|
-
)
|
|
151
|
-
|
|
152
|
-
// Frame specific items
|
|
153
|
-
position = .item(MKMapItem.forCurrentLocation())
|
|
154
|
-
position = .rect(MKMapRect(...))
|
|
155
|
-
```
|
|
133
|
+
interact and to programmatically move the camera. Initialize it with `.region`,
|
|
134
|
+
`.userLocation(fallback:)`, `.camera`, `.item`, `.rect`, or `.automatic`. See
|
|
135
|
+
[references/mapkit-patterns.md](references/mapkit-patterns.md) ("Camera Control") for the position
|
|
136
|
+
initializers, animated fly-to, framing content, and reading the visible region.
|
|
156
137
|
|
|
157
138
|
### Map Style
|
|
158
139
|
|
|
@@ -308,76 +289,39 @@ if let placemark = placemarks.first {
|
|
|
308
289
|
|
|
309
290
|
### MKGeocodingRequest and MKReverseGeocodingRequest (iOS 26+)
|
|
310
291
|
|
|
311
|
-
New MapKit-native geocoding
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
func reverseGeocode(location: CLLocation) async throws -> MKMapItem? {
|
|
317
|
-
guard let request = MKReverseGeocodingRequest(location: location) else {
|
|
318
|
-
return nil
|
|
319
|
-
}
|
|
320
|
-
let mapItems = try await request.mapItems
|
|
321
|
-
return mapItems.first
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
@available(iOS 26, *)
|
|
325
|
-
func forwardGeocode(address: String) async throws -> [MKMapItem] {
|
|
326
|
-
guard let request = MKGeocodingRequest(addressString: address) else { return [] }
|
|
327
|
-
return try await request.mapItems
|
|
328
|
-
}
|
|
329
|
-
```
|
|
292
|
+
New MapKit-native geocoding returns `MKMapItem` with richer data and `MKAddress`
|
|
293
|
+
/ `MKAddressRepresentations` for flexible address formatting. Construct
|
|
294
|
+
`MKGeocodingRequest(addressString:)` or `MKReverseGeocodingRequest(location:)` and
|
|
295
|
+
await `.mapItems`. See [references/mapkit-patterns.md](references/mapkit-patterns.md) ("iOS 26 New
|
|
296
|
+
APIs") for both, plus `MKAddressRepresentations` formatting.
|
|
330
297
|
|
|
331
298
|
## Search
|
|
332
299
|
|
|
333
300
|
### MKLocalSearchCompleter (Autocomplete)
|
|
334
301
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
var results: [MKLocalSearchCompletion] = []
|
|
339
|
-
var query: String = "" { didSet { completer.queryFragment = query } }
|
|
340
|
-
|
|
341
|
-
private let completer = MKLocalSearchCompleter()
|
|
342
|
-
|
|
343
|
-
override init() {
|
|
344
|
-
super.init()
|
|
345
|
-
completer.delegate = self
|
|
346
|
-
completer.resultTypes = [.address, .pointOfInterest]
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
|
|
350
|
-
results = completer.results
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) {
|
|
354
|
-
results = []
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
```
|
|
302
|
+
Set `queryFragment` on an `MKLocalSearchCompleter` and read `results` from its
|
|
303
|
+
delegate for suggestions. Debounce input (300ms+) and constrain `completer.region`
|
|
304
|
+
to the visible map region.
|
|
358
305
|
|
|
359
306
|
### MKLocalSearch (Full Search)
|
|
360
307
|
|
|
308
|
+
Convert a selected completion (or a natural-language query) into full `MKMapItem`
|
|
309
|
+
results:
|
|
310
|
+
|
|
361
311
|
```swift
|
|
362
312
|
func search(for completion: MKLocalSearchCompletion) async throws -> [MKMapItem] {
|
|
363
313
|
let request = MKLocalSearch.Request(completion: completion)
|
|
364
314
|
request.resultTypes = [.pointOfInterest, .address]
|
|
365
|
-
let
|
|
366
|
-
let response = try await search.start()
|
|
367
|
-
return response.mapItems
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// Search by natural language query within a region
|
|
371
|
-
func searchNearby(query: String, region: MKCoordinateRegion) async throws -> [MKMapItem] {
|
|
372
|
-
let request = MKLocalSearch.Request()
|
|
373
|
-
request.naturalLanguageQuery = query
|
|
374
|
-
request.region = region
|
|
375
|
-
let search = MKLocalSearch(request: request)
|
|
376
|
-
let response = try await search.start()
|
|
315
|
+
let response = try await MKLocalSearch(request: request).start()
|
|
377
316
|
return response.mapItems
|
|
378
317
|
}
|
|
379
318
|
```
|
|
380
319
|
|
|
320
|
+
For a natural-language query, set `request.naturalLanguageQuery` and
|
|
321
|
+
`request.region` instead. See [references/mapkit-patterns.md](references/mapkit-patterns.md) ("Search
|
|
322
|
+
with Autocomplete") for the full completer delegate class and a `.searchable`
|
|
323
|
+
view integration.
|
|
324
|
+
|
|
381
325
|
## Directions
|
|
382
326
|
|
|
383
327
|
```swift
|
|
@@ -411,32 +355,13 @@ Map {
|
|
|
411
355
|
}
|
|
412
356
|
```
|
|
413
357
|
|
|
414
|
-
### ETA
|
|
358
|
+
### ETA and Cycling Directions
|
|
415
359
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
let directions = MKDirections(request: request)
|
|
422
|
-
let response = try await directions.calculateETA()
|
|
423
|
-
return response.expectedTravelTime
|
|
424
|
-
}
|
|
425
|
-
```
|
|
426
|
-
|
|
427
|
-
### Cycling Directions (iOS 14+)
|
|
428
|
-
|
|
429
|
-
```swift
|
|
430
|
-
func getCyclingDirections(to destination: MKMapItem) async throws -> MKRoute? {
|
|
431
|
-
let request = MKDirections.Request()
|
|
432
|
-
request.source = MKMapItem.forCurrentLocation()
|
|
433
|
-
request.destination = destination
|
|
434
|
-
request.transportType = .cycling
|
|
435
|
-
let directions = MKDirections(request: request)
|
|
436
|
-
let response = try await directions.calculate()
|
|
437
|
-
return response.routes.first
|
|
438
|
-
}
|
|
439
|
-
```
|
|
360
|
+
For travel time only, use `MKDirections.calculateETA()` (returns
|
|
361
|
+
`expectedTravelTime` without route geometry). For bike routes, set
|
|
362
|
+
`request.transportType = .cycling` (iOS 14+). See
|
|
363
|
+
[references/mapkit-patterns.md](references/mapkit-patterns.md) ("ETA Calculation" and "Cycling
|
|
364
|
+
Directions") for both.
|
|
440
365
|
|
|
441
366
|
## PlaceDescriptor (iOS 26+)
|
|
442
367
|
|
|
@@ -137,6 +137,33 @@ Annotation(place.name, coordinate: place.coordinate, anchor: .bottom) {
|
|
|
137
137
|
|
|
138
138
|
## Camera Control (MapCameraPosition)
|
|
139
139
|
|
|
140
|
+
### Setting an initial position
|
|
141
|
+
|
|
142
|
+
`MapCameraPosition` controls what the map displays. Bind it to let the user
|
|
143
|
+
interact and to move the camera programmatically.
|
|
144
|
+
|
|
145
|
+
```swift
|
|
146
|
+
// Center on a region
|
|
147
|
+
@State private var position: MapCameraPosition = .region(
|
|
148
|
+
MKCoordinateRegion(
|
|
149
|
+
center: CLLocationCoordinate2D(latitude: 37.334, longitude: -122.009),
|
|
150
|
+
span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
// Follow user location
|
|
155
|
+
@State private var position: MapCameraPosition = .userLocation(fallback: .automatic)
|
|
156
|
+
|
|
157
|
+
// Specific camera angle (3D perspective)
|
|
158
|
+
@State private var position: MapCameraPosition = .camera(
|
|
159
|
+
MapCamera(centerCoordinate: applePark, distance: 1000, heading: 90, pitch: 60)
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
// Frame specific items
|
|
163
|
+
position = .item(MKMapItem.forCurrentLocation())
|
|
164
|
+
position = .rect(MKMapRect(...))
|
|
165
|
+
```
|
|
166
|
+
|
|
140
167
|
### Animate camera changes
|
|
141
168
|
|
|
142
169
|
Wrap position updates in `withAnimation` for smooth transitions:
|
|
@@ -389,6 +416,21 @@ ForEach(Array(response.routes.enumerated()), id: \.offset) { index, route in
|
|
|
389
416
|
}
|
|
390
417
|
```
|
|
391
418
|
|
|
419
|
+
### ETA Calculation
|
|
420
|
+
|
|
421
|
+
Use `calculateETA()` when you only need travel time, not the full route geometry.
|
|
422
|
+
|
|
423
|
+
```swift
|
|
424
|
+
func getETA(from source: MKMapItem, to destination: MKMapItem) async throws -> TimeInterval {
|
|
425
|
+
let request = MKDirections.Request()
|
|
426
|
+
request.source = source
|
|
427
|
+
request.destination = destination
|
|
428
|
+
let directions = MKDirections(request: request)
|
|
429
|
+
let response = try await directions.calculateETA()
|
|
430
|
+
return response.expectedTravelTime
|
|
431
|
+
}
|
|
432
|
+
```
|
|
433
|
+
|
|
392
434
|
---
|
|
393
435
|
|
|
394
436
|
## Look Around Preview
|