@mmerterden/multi-agent-pipeline 20.36.0 → 20.37.1
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 +37 -0
- package/docs/facts.json +2 -2
- package/docs/features.md +1 -1
- package/docs/recovery-guide.md +1 -1
- package/install/catalog-history.json +1 -1
- package/manifest.json +225 -148
- package/package.json +1 -1
- package/pipeline/commands/multi-agent/SKILL.md +33 -154
- package/pipeline/commands/multi-agent/analysis/SKILL.md +14 -68
- package/pipeline/commands/multi-agent/analysis-jira/SKILL.md +5 -8
- package/pipeline/commands/multi-agent/autopilot-on/SKILL.md +38 -109
- package/pipeline/commands/multi-agent/channels/SKILL.md +38 -167
- package/pipeline/commands/multi-agent/complaint-analysis/SKILL.md +37 -144
- package/pipeline/commands/multi-agent/design-check/SKILL.md +27 -131
- package/pipeline/commands/multi-agent/garbage-collect/SKILL.md +22 -48
- package/pipeline/commands/multi-agent/ios-coding-standard/SKILL.md +37 -180
- package/pipeline/commands/multi-agent/jira/SKILL.md +16 -46
- package/pipeline/commands/multi-agent/manual-test/SKILL.md +12 -7
- package/pipeline/commands/multi-agent/refactor/SKILL.md +23 -184
- package/pipeline/commands/multi-agent/resume/SKILL.md +30 -136
- package/pipeline/commands/multi-agent/review/SKILL.md +24 -199
- package/pipeline/commands/multi-agent/setup/SKILL.md +25 -181
- package/pipeline/commands/multi-agent/stack/SKILL.md +9 -53
- package/pipeline/commands/multi-agent/status/SKILL.md +13 -2
- package/pipeline/commands/multi-agent/store-ready/SKILL.md +27 -226
- package/pipeline/commands/multi-agent/sync/SKILL.md +35 -164
- package/pipeline/commands/multi-agent/test-gen/SKILL.md +2 -4
- package/pipeline/commands/multi-agent/update/SKILL.md +18 -60
- package/pipeline/contract/CHANGELOG.md +9 -0
- package/pipeline/contract/fixtures/error-invalid-request.json +1 -1
- package/pipeline/contract/fixtures/error-unauthorized.json +1 -1
- package/pipeline/contract/fixtures/error-unsigned.json +1 -1
- package/pipeline/contract/fixtures/runs-awaiting-question.json +1 -1
- package/pipeline/contract/fixtures/runs-empty.json +1 -1
- package/pipeline/contract/fixtures/runs-failed.json +1 -1
- package/pipeline/contract/fixtures/runs-pr-opened-redacted.json +1 -1
- package/pipeline/contract/fixtures/runs-pr-opened.json +1 -1
- package/pipeline/contract/fixtures/runs-running.json +1 -1
- package/pipeline/contract/manifest.json +5 -5
- package/pipeline/contract/types/index.d.ts +3 -2
- package/pipeline/multi-agent-refs/audit-guide.md +20 -0
- package/pipeline/multi-agent-refs/autopilot-on/details.md +148 -0
- package/pipeline/multi-agent-refs/channels/generator.md +14 -0
- package/pipeline/multi-agent-refs/channels/preview.md +13 -0
- package/pipeline/multi-agent-refs/channels/reference.md +46 -0
- package/pipeline/multi-agent-refs/complaint-analysis/phases.md +159 -0
- package/pipeline/multi-agent-refs/design-check/export.md +1 -0
- package/pipeline/multi-agent-refs/design-check/init-inventory.md +1 -1
- package/pipeline/multi-agent-refs/design-check/scope-and-ledger.md +64 -0
- package/pipeline/multi-agent-refs/features/analysis-command-notes.md +53 -0
- package/pipeline/multi-agent-refs/features/commit-multi-repo.md +63 -0
- package/pipeline/multi-agent-refs/features/dev-multi-repo.md +51 -0
- package/pipeline/multi-agent-refs/features/test-gap.md +40 -0
- package/pipeline/multi-agent-refs/features/worktree-finalize.md +1 -1
- package/pipeline/multi-agent-refs/garbage-collect/phases.md +60 -0
- package/pipeline/multi-agent-refs/ios-coding-standard/plan.md +71 -0
- package/pipeline/multi-agent-refs/ios-coding-standard/resolve-and-scan.md +135 -0
- package/pipeline/multi-agent-refs/jira/picker-detail.md +71 -0
- package/pipeline/multi-agent-refs/orchestrator/copilot-pipeline.md +88 -0
- package/pipeline/multi-agent-refs/orchestrator/dispatcher.md +96 -1
- package/pipeline/multi-agent-refs/orchestrator/phase-3-user-test.md +12 -9
- package/pipeline/multi-agent-refs/phases/phase-1-plan.md +4 -4
- package/pipeline/multi-agent-refs/phases/phase-2-dev.md +8 -47
- package/pipeline/multi-agent-refs/phases/phase-3-review.md +21 -65
- package/pipeline/multi-agent-refs/phases/phase-4-commit.md +9 -59
- package/pipeline/multi-agent-refs/phases/phase-5-report.md +0 -31
- package/pipeline/multi-agent-refs/refactor/sweep.md +190 -0
- package/pipeline/multi-agent-refs/resume/details.md +115 -2
- package/pipeline/multi-agent-refs/review/inputs.md +33 -1
- package/pipeline/multi-agent-refs/review/module-guides.md +9 -1
- package/pipeline/multi-agent-refs/review/panel.md +75 -0
- package/pipeline/multi-agent-refs/review/valven.md +5 -1
- package/pipeline/multi-agent-refs/rules.md +1 -1
- package/pipeline/multi-agent-refs/setup/hooks-and-plugins.md +4 -0
- package/pipeline/multi-agent-refs/setup/token-save-flow.md +13 -0
- package/pipeline/multi-agent-refs/stack/background.md +72 -0
- package/pipeline/multi-agent-refs/store-ready/gates.md +139 -0
- package/pipeline/multi-agent-refs/store-ready/report.md +40 -2
- package/pipeline/multi-agent-refs/sync/office.md +10 -0
- package/pipeline/multi-agent-refs/sync/overview.md +61 -0
- package/pipeline/multi-agent-refs/sync/repo.md +91 -0
- package/pipeline/multi-agent-refs/update/details.md +83 -0
- package/pipeline/office/index.html +6 -3
- package/pipeline/office/toolkit.json +1 -1
- package/pipeline/rules/figma-pipeline.md +6 -3
- package/pipeline/schemas/prefs.schema.json +1 -1
- package/pipeline/schemas/runs-index.schema.json +2 -1
- package/pipeline/scripts/_memory-policy.mjs +27 -5
- package/pipeline/scripts/checkpoint-fold.sh +59 -0
- package/pipeline/scripts/contract-server.mjs +1 -1
- package/pipeline/scripts/gate-ledger.mjs +1 -1
- package/pipeline/scripts/office-control.mjs +41 -15
- package/pipeline/scripts/offload-ref.sh +1 -1
- package/pipeline/scripts/rework-rulings.mjs +5 -3
- package/pipeline/scripts/runs-index.mjs +34 -7
- package/pipeline/scripts/scope-check-gate.mjs +15 -2
- package/pipeline/scripts/status-overview.mjs +401 -0
- package/pipeline/scripts/user-test-handoff.sh +203 -0
- package/pipeline/scripts/validate-planning.mjs +5 -3
- package/pipeline/scripts/validate-triage.mjs +2 -1
- package/pipeline/scripts/write-state.mjs +13 -4
- package/pipeline/skills/.skill-manifest.json +64 -64
- package/pipeline/skills/shared/core/apple-archive-compliance/SKILL.md +7 -70
- package/pipeline/skills/shared/core/apple-archive-compliance/references/examples.md +67 -0
- package/pipeline/skills/shared/core/apple-archive-compliance/references/version-history.md +8 -0
- package/pipeline/skills/shared/core/google-play-compliance/SKILL.md +10 -72
- package/pipeline/skills/shared/core/google-play-compliance/references/examples.md +69 -0
- package/pipeline/skills/shared/core/google-play-compliance/references/notes.md +7 -0
- package/pipeline/skills/shared/core/google-play-compliance/references/version-history.md +5 -0
- package/pipeline/skills/shared/core/multi-agent/SKILL.md +39 -262
- package/pipeline/skills/shared/core/multi-agent-autopilot-on/SKILL.md +35 -73
- package/pipeline/skills/shared/core/multi-agent-channels/SKILL.md +37 -337
- package/pipeline/skills/shared/core/multi-agent-design-check/SKILL.md +27 -122
- package/pipeline/skills/shared/core/multi-agent-help/SKILL.md +8 -340
- package/pipeline/skills/shared/core/multi-agent-ios-coding-standard/SKILL.md +37 -180
- package/pipeline/skills/shared/core/multi-agent-manual-test/SKILL.md +12 -7
- package/pipeline/skills/shared/core/multi-agent-refactor/SKILL.md +23 -184
- package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +14 -82
- package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +25 -277
- package/pipeline/skills/shared/core/multi-agent-status/SKILL.md +13 -2
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +73 -217
- package/pipeline/skills/shared/external/alarmkit/SKILL.md +11 -162
- package/pipeline/skills/shared/external/alarmkit/references/alarmkit-code.md +257 -0
- package/pipeline/skills/shared/external/android-architecture/SKILL.md +7 -58
- package/pipeline/skills/shared/external/android-architecture/references/code-examples.md +68 -0
- package/pipeline/skills/shared/external/app-intents/SKILL.md +3 -45
- package/pipeline/skills/shared/external/app-intents/references/code-examples.md +54 -0
- package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +5 -81
- package/pipeline/skills/shared/external/app-store-optimization/references/engagement-and-localization.md +96 -0
- package/pipeline/skills/shared/external/app-store-review/SKILL.md +6 -112
- package/pipeline/skills/shared/external/app-store-review/references/monetization-and-submission.md +138 -0
- package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +10 -117
- package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models-code.md +186 -0
- package/pipeline/skills/shared/external/authentication/SKILL.md +3 -50
- package/pipeline/skills/shared/external/authentication/references/code-examples.md +59 -0
- package/pipeline/skills/shared/external/callkit-voip/SKILL.md +3 -84
- package/pipeline/skills/shared/external/callkit-voip/references/code-examples.md +93 -0
- package/pipeline/skills/shared/external/ci-cd-pipelines/SKILL.md +7 -263
- package/pipeline/skills/shared/external/ci-cd-pipelines/references/pipeline-examples.md +281 -0
- package/pipeline/skills/shared/external/clean-code/SKILL.md +8 -41
- package/pipeline/skills/shared/external/clean-code/references/code-examples.md +56 -0
- package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +8 -146
- package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-code.md +214 -0
- package/pipeline/skills/shared/external/compose-components/SKILL.md +6 -80
- package/pipeline/skills/shared/external/compose-components/references/code-examples.md +89 -0
- package/pipeline/skills/shared/external/compose-navigation/SKILL.md +5 -64
- package/pipeline/skills/shared/external/compose-navigation/references/code-examples.md +70 -0
- package/pipeline/skills/shared/external/core-data/SKILL.md +9 -157
- package/pipeline/skills/shared/external/core-data/references/core-data-code.md +221 -0
- package/pipeline/skills/shared/external/core-nfc/SKILL.md +3 -89
- package/pipeline/skills/shared/external/core-nfc/references/code-examples.md +98 -0
- package/pipeline/skills/shared/external/coreml/SKILL.md +17 -133
- package/pipeline/skills/shared/external/coreml/references/coreml-code.md +208 -0
- package/pipeline/skills/shared/external/css-modern/SKILL.md +8 -131
- package/pipeline/skills/shared/external/css-modern/references/code-examples.md +142 -0
- package/pipeline/skills/shared/external/database-patterns/SKILL.md +6 -103
- package/pipeline/skills/shared/external/database-patterns/references/orm-examples.md +112 -0
- package/pipeline/skills/shared/external/html-semantic/SKILL.md +7 -48
- package/pipeline/skills/shared/external/html-semantic/references/code-examples.md +54 -0
- package/pipeline/skills/shared/external/humanizer/SKILL.md +6 -19
- package/pipeline/skills/shared/external/humanizer/references/voice.md +24 -0
- package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +13 -170
- package/pipeline/skills/shared/external/ios-accessibility/references/assistive-tech-review.md +224 -0
- package/pipeline/skills/shared/external/ios-localization/SKILL.md +2 -24
- package/pipeline/skills/shared/external/ios-localization/references/code-examples.md +30 -0
- package/pipeline/skills/shared/external/ios-networking/SKILL.md +3 -58
- package/pipeline/skills/shared/external/ios-networking/references/code-examples.md +67 -0
- package/pipeline/skills/shared/external/ios-security/SKILL.md +4 -64
- package/pipeline/skills/shared/external/ios-security/references/code-examples.md +74 -0
- package/pipeline/skills/shared/external/live-activities/SKILL.md +4 -90
- package/pipeline/skills/shared/external/live-activities/references/code-examples.md +108 -0
- package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +9 -23
- package/pipeline/skills/shared/external/localization-reuse-map/references/project-settings.md +22 -0
- package/pipeline/skills/shared/external/mapkit-location/SKILL.md +5 -95
- package/pipeline/skills/shared/external/mapkit-location/references/code-examples.md +117 -0
- package/pipeline/skills/shared/external/nextjs-app-router/SKILL.md +7 -106
- package/pipeline/skills/shared/external/nextjs-app-router/references/app-router-examples.md +124 -0
- package/pipeline/skills/shared/external/permissionkit/SKILL.md +2 -31
- package/pipeline/skills/shared/external/permissionkit/references/code-examples.md +37 -0
- package/pipeline/skills/shared/external/play-store-review/SKILL.md +6 -74
- package/pipeline/skills/shared/external/play-store-review/references/signing-and-policies.md +83 -0
- package/pipeline/skills/shared/external/push-notifications/SKILL.md +8 -125
- package/pipeline/skills/shared/external/push-notifications/references/notification-code.md +176 -0
- package/pipeline/skills/shared/external/python-patterns/SKILL.md +7 -158
- package/pipeline/skills/shared/external/python-patterns/references/python-examples.md +176 -0
- package/pipeline/skills/shared/external/rest-api-design/SKILL.md +7 -157
- package/pipeline/skills/shared/external/rest-api-design/references/api-examples.md +175 -0
- package/pipeline/skills/shared/external/swift-architecture/SKILL.md +11 -230
- package/pipeline/skills/shared/external/swift-architecture/references/pattern-examples.md +295 -0
- package/pipeline/skills/shared/external/swift-codable/SKILL.md +17 -149
- package/pipeline/skills/shared/external/swift-codable/references/codable-recipes.md +207 -0
- package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +12 -99
- package/pipeline/skills/shared/external/swift-concurrency/references/code-examples.md +100 -0
- package/pipeline/skills/shared/external/swift-concurrency/references/swift-6-2-additions.md +18 -0
- package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +9 -78
- package/pipeline/skills/shared/external/swift-formatstyle/references/formatstyle-examples.md +113 -0
- package/pipeline/skills/shared/external/swift-language/SKILL.md +3 -56
- package/pipeline/skills/shared/external/swift-language/references/code-examples.md +65 -0
- package/pipeline/skills/shared/external/swift-testing-pro/SKILL.md +1 -0
- package/pipeline/skills/shared/external/swiftdata/SKILL.md +6 -122
- package/pipeline/skills/shared/external/swiftdata/references/code-examples.md +140 -0
- package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +12 -139
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/interop-code.md +198 -0
- package/pipeline/skills/shared/external/tailwind-css/SKILL.md +9 -81
- package/pipeline/skills/shared/external/tailwind-css/references/code-examples.md +91 -0
- package/pipeline/skills/shared/external/testing-backend/SKILL.md +7 -207
- package/pipeline/skills/shared/external/testing-backend/references/testing-examples.md +225 -0
- package/pipeline/skills/shared/external/typescript-patterns/SKILL.md +8 -89
- package/pipeline/skills/shared/external/typescript-patterns/references/code-examples.md +100 -0
- package/pipeline/skills/shared/external/vision-framework/SKILL.md +2 -20
- package/pipeline/skills/shared/external/vision-framework/references/code-examples.md +26 -0
- package/pipeline/skills/shared/external/vue-composition/SKILL.md +7 -169
- package/pipeline/skills/shared/external/vue-composition/references/vue-examples.md +187 -0
- package/pipeline/skills/shared/external/web-accessibility/SKILL.md +7 -174
- package/pipeline/skills/shared/external/web-accessibility/references/a11y-examples.md +192 -0
- package/pipeline/skills/shared/external/web-performance/SKILL.md +8 -66
- package/pipeline/skills/shared/external/web-performance/references/code-examples.md +77 -0
- package/pipeline/skills/shared/external/web-testing/SKILL.md +11 -186
- package/pipeline/skills/shared/external/web-testing/references/testing-examples.md +213 -0
- /package/pipeline/skills/shared/external/localization-reuse-map/{reference → references}/format-and-output.md +0 -0
- /package/pipeline/skills/shared/external/localization-reuse-map/{reference → references}/publish-and-snapshot.md +0 -0
- /package/pipeline/skills/shared/external/localization-reuse-map/{reference → references}/sources-and-recipes.md +0 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# iOS Networking Code Examples
|
|
2
|
+
|
|
3
|
+
Full code samples for the SKILL.md sections that link here. The rules and the decision guidance stay in SKILL.md; each heading below matches the SKILL.md section the code belongs to.
|
|
4
|
+
|
|
5
|
+
## Protocol-based client
|
|
6
|
+
|
|
7
|
+
```swift
|
|
8
|
+
protocol APIClientProtocol: Sendable {
|
|
9
|
+
func fetch<Reply: Decodable & Sendable>(_ endpoint: Endpoint, as kind: Reply.Type) async throws -> Reply
|
|
10
|
+
func send<Reply: Decodable & Sendable>(_ endpoint: Endpoint, json payload: some Encodable & Sendable,
|
|
11
|
+
as kind: Reply.Type) async throws -> Reply
|
|
12
|
+
func perform(_ endpoint: Endpoint) async throws
|
|
13
|
+
func upload<Reply: Decodable & Sendable>(_ endpoint: Endpoint, bytes: Data,
|
|
14
|
+
as kind: Reply.Type) async throws -> Reply
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
struct Endpoint: Sendable {
|
|
18
|
+
enum HTTPMethod: String, Sendable { case get, post, put, patch, delete }
|
|
19
|
+
|
|
20
|
+
var path: String
|
|
21
|
+
var method: HTTPMethod = .get
|
|
22
|
+
var queryItems: [URLQueryItem] = []
|
|
23
|
+
var headers: [String: String] = [:]
|
|
24
|
+
var body: Data? = nil
|
|
25
|
+
var cachePolicy: URLRequest.CachePolicy = .useProtocolCachePolicy
|
|
26
|
+
var timeoutInterval: TimeInterval = 30
|
|
27
|
+
|
|
28
|
+
func urlRequest(relativeTo baseURL: URL) throws -> URLRequest {
|
|
29
|
+
guard var parts = URLComponents(url: baseURL.appending(path: path),
|
|
30
|
+
resolvingAgainstBaseURL: true) else {
|
|
31
|
+
throw NetworkError.invalidURL
|
|
32
|
+
}
|
|
33
|
+
if !queryItems.isEmpty { parts.queryItems = queryItems }
|
|
34
|
+
guard let url = parts.url else { throw NetworkError.invalidURL }
|
|
35
|
+
var request = URLRequest(url: url, cachePolicy: cachePolicy, timeoutInterval: timeoutInterval)
|
|
36
|
+
request.httpMethod = method.rawValue.uppercased()
|
|
37
|
+
request.httpBody = body
|
|
38
|
+
request.allHTTPHeaderFields = headers
|
|
39
|
+
return request
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Retry with exponential backoff
|
|
45
|
+
|
|
46
|
+
```swift
|
|
47
|
+
func withRetry<T: Sendable>(
|
|
48
|
+
maxAttempts: Int = 3,
|
|
49
|
+
initialDelay: Duration = .seconds(1),
|
|
50
|
+
maxDelay: Duration = .seconds(30),
|
|
51
|
+
shouldRetry: @Sendable (any Error) -> Bool = isTransient,
|
|
52
|
+
operation: @Sendable () async throws -> T
|
|
53
|
+
) async throws -> T {
|
|
54
|
+
precondition(maxAttempts > 0, "maxAttempts must be at least 1")
|
|
55
|
+
for attempt in 0..<maxAttempts {
|
|
56
|
+
try Task.checkCancellation()
|
|
57
|
+
do {
|
|
58
|
+
return try await operation()
|
|
59
|
+
} catch {
|
|
60
|
+
guard attempt < maxAttempts - 1, shouldRetry(error) else { throw error }
|
|
61
|
+
let capped = min(initialDelay * (1 << attempt), maxDelay)
|
|
62
|
+
try await Task.sleep(for: capped + capped * Double.random(in: 0...0.1))
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
preconditionFailure("the last attempt always returns or throws")
|
|
66
|
+
}
|
|
67
|
+
```
|
|
@@ -32,34 +32,9 @@ passwords, tokens, API keys, or secrets in UserDefaults, files, or Core Data.
|
|
|
32
32
|
|
|
33
33
|
### Storing Credentials
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
func saveToKeychain(account: String, data: Data, service: String) throws {
|
|
37
|
-
let query: [String: Any] = [
|
|
38
|
-
kSecClass as String: kSecClassGenericPassword,
|
|
39
|
-
kSecAttrAccount as String: account,
|
|
40
|
-
kSecAttrService as String: service,
|
|
41
|
-
kSecValueData as String: data,
|
|
42
|
-
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
|
|
43
|
-
]
|
|
35
|
+
Save with `SecItemAdd` and an explicit accessibility class; when the item already exists (`errSecDuplicateItem`), update it with `SecItemUpdate` instead of failing.
|
|
44
36
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if status == errSecDuplicateItem {
|
|
48
|
-
let updateQuery: [String: Any] = [
|
|
49
|
-
kSecClass as String: kSecClassGenericPassword,
|
|
50
|
-
kSecAttrAccount as String: account,
|
|
51
|
-
kSecAttrService as String: service
|
|
52
|
-
]
|
|
53
|
-
let updates: [String: Any] = [kSecValueData as String: data]
|
|
54
|
-
let updateStatus = SecItemUpdate(updateQuery as CFDictionary, updates as CFDictionary)
|
|
55
|
-
guard updateStatus == errSecSuccess else {
|
|
56
|
-
throw KeychainError.updateFailed(updateStatus)
|
|
57
|
-
}
|
|
58
|
-
} else if status != errSecSuccess {
|
|
59
|
-
throw KeychainError.saveFailed(status)
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
```
|
|
37
|
+
Code: [Storing Credentials](references/code-examples.md#storing-credentials).
|
|
63
38
|
|
|
64
39
|
### Reading Credentials
|
|
65
40
|
|
|
@@ -364,43 +339,7 @@ Pin certificates for sensitive API connections to prevent MITM attacks.
|
|
|
364
339
|
|
|
365
340
|
### URLSession Delegate Pinning
|
|
366
341
|
|
|
367
|
-
|
|
368
|
-
import CryptoKit
|
|
369
|
-
|
|
370
|
-
class PinnedSessionDelegate: NSObject, URLSessionDelegate {
|
|
371
|
-
// SHA-256 hash of the certificate's Subject Public Key Info
|
|
372
|
-
private let pinnedHashes: Set<String> = [
|
|
373
|
-
"base64EncodedSHA256HashOfSPKI=="
|
|
374
|
-
]
|
|
375
|
-
|
|
376
|
-
func urlSession(
|
|
377
|
-
_ session: URLSession,
|
|
378
|
-
didReceive challenge: URLAuthenticationChallenge
|
|
379
|
-
) async -> (URLSession.AuthChallengeDisposition, URLCredential?) {
|
|
380
|
-
guard let trust = challenge.protectionSpace.serverTrust,
|
|
381
|
-
let chain = SecTrustCopyCertificateChain(trust) as? [SecCertificate],
|
|
382
|
-
let certificate = chain.first else {
|
|
383
|
-
return (.cancelAuthenticationChallenge, nil)
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
guard let publicKey = SecCertificateCopyKey(certificate),
|
|
387
|
-
let publicKeyData = SecKeyCopyExternalRepresentation(
|
|
388
|
-
publicKey, nil
|
|
389
|
-
) as Data? else {
|
|
390
|
-
return (.cancelAuthenticationChallenge, nil)
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
let hash = SHA256.hash(data: publicKeyData)
|
|
394
|
-
let hashString = Data(hash).base64EncodedString()
|
|
395
|
-
|
|
396
|
-
if pinnedHashes.contains(hashString) {
|
|
397
|
-
return (.useCredential, URLCredential(trust: trust))
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
return (.cancelAuthenticationChallenge, nil)
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
```
|
|
342
|
+
Code: [URLSession Delegate Pinning](references/code-examples.md#urlsession-delegate-pinning).
|
|
404
343
|
|
|
405
344
|
Rules:
|
|
406
345
|
- Pin the public key hash, not the certificate. Certificates rotate; public keys are more stable.
|
|
@@ -495,4 +434,5 @@ compliance checklists, see `references/app-review-guidelines.md`.
|
|
|
495
434
|
- Privacy manifest guide: `references/privacy-manifest.md`
|
|
496
435
|
- App Review security guidelines: `references/app-review-guidelines.md`
|
|
497
436
|
- File storage directory guide and protection: `references/file-storage-patterns.md`
|
|
437
|
+
- [Code examples](references/code-examples.md): open for the full code behind Storing Credentials, URLSession Delegate Pinning.
|
|
498
438
|
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# iOS Security Code Examples
|
|
2
|
+
|
|
3
|
+
Full code samples for the SKILL.md sections that link here. The rules and the decision guidance stay in SKILL.md; each heading below matches the SKILL.md section the code belongs to.
|
|
4
|
+
|
|
5
|
+
## Storing Credentials
|
|
6
|
+
|
|
7
|
+
```swift
|
|
8
|
+
func saveToKeychain(account: String, data: Data, service: String) throws {
|
|
9
|
+
let query: [String: Any] = [
|
|
10
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
11
|
+
kSecAttrAccount as String: account,
|
|
12
|
+
kSecAttrService as String: service,
|
|
13
|
+
kSecValueData as String: data,
|
|
14
|
+
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
let status = SecItemAdd(query as CFDictionary, nil)
|
|
18
|
+
|
|
19
|
+
if status == errSecDuplicateItem {
|
|
20
|
+
let updateQuery: [String: Any] = [
|
|
21
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
22
|
+
kSecAttrAccount as String: account,
|
|
23
|
+
kSecAttrService as String: service
|
|
24
|
+
]
|
|
25
|
+
let updates: [String: Any] = [kSecValueData as String: data]
|
|
26
|
+
let updateStatus = SecItemUpdate(updateQuery as CFDictionary, updates as CFDictionary)
|
|
27
|
+
guard updateStatus == errSecSuccess else {
|
|
28
|
+
throw KeychainError.updateFailed(updateStatus)
|
|
29
|
+
}
|
|
30
|
+
} else if status != errSecSuccess {
|
|
31
|
+
throw KeychainError.saveFailed(status)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## URLSession Delegate Pinning
|
|
37
|
+
|
|
38
|
+
```swift
|
|
39
|
+
import CryptoKit
|
|
40
|
+
|
|
41
|
+
class PinnedSessionDelegate: NSObject, URLSessionDelegate {
|
|
42
|
+
// SHA-256 hash of the certificate's Subject Public Key Info
|
|
43
|
+
private let pinnedHashes: Set<String> = [
|
|
44
|
+
"base64EncodedSHA256HashOfSPKI=="
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
func urlSession(
|
|
48
|
+
_ session: URLSession,
|
|
49
|
+
didReceive challenge: URLAuthenticationChallenge
|
|
50
|
+
) async -> (URLSession.AuthChallengeDisposition, URLCredential?) {
|
|
51
|
+
guard let trust = challenge.protectionSpace.serverTrust,
|
|
52
|
+
let chain = SecTrustCopyCertificateChain(trust) as? [SecCertificate],
|
|
53
|
+
let certificate = chain.first else {
|
|
54
|
+
return (.cancelAuthenticationChallenge, nil)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
guard let publicKey = SecCertificateCopyKey(certificate),
|
|
58
|
+
let publicKeyData = SecKeyCopyExternalRepresentation(
|
|
59
|
+
publicKey, nil
|
|
60
|
+
) as Data? else {
|
|
61
|
+
return (.cancelAuthenticationChallenge, nil)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let hash = SHA256.hash(data: publicKeyData)
|
|
65
|
+
let hashString = Data(hash).base64EncodedString()
|
|
66
|
+
|
|
67
|
+
if pinnedHashes.contains(hashString) {
|
|
68
|
+
return (.useCredential, URLCredential(trust: trust))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return (.cancelAuthenticationChallenge, nil)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
@@ -62,34 +62,7 @@ Two layers, with different lifetimes:
|
|
|
62
62
|
- `ContentState` carries whatever changes. The entire value is encoded on
|
|
63
63
|
every update and in every push, so keep it lean.
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
import ActivityKit
|
|
67
|
-
|
|
68
|
-
struct ChargingAttributes: ActivityAttributes {
|
|
69
|
-
struct ContentState: Codable, Hashable {
|
|
70
|
-
var batteryPercent: Int
|
|
71
|
-
var finishWindow: ClosedRange<Date>
|
|
72
|
-
var phase: ChargePhase
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
var stationName: String
|
|
76
|
-
var connectorLabel: String
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
enum ChargePhase: String, Codable, Hashable, CaseIterable {
|
|
80
|
-
case queued, connecting, charging, balancing, complete
|
|
81
|
-
|
|
82
|
-
var symbolName: String {
|
|
83
|
-
switch self {
|
|
84
|
-
case .queued: "hourglass"
|
|
85
|
-
case .connecting: "powerplug"
|
|
86
|
-
case .charging: "bolt.car"
|
|
87
|
-
case .balancing: "slider.horizontal.3"
|
|
88
|
-
case .complete: "checkmark.circle.fill"
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
```
|
|
65
|
+
Code: [Modelling the data](references/code-examples.md#modelling-the-data).
|
|
93
66
|
|
|
94
67
|
### Stale date
|
|
95
68
|
|
|
@@ -114,25 +87,7 @@ let snapshot = ActivityContent(
|
|
|
114
87
|
`pushType: .token` when a server should be able to update it. The
|
|
115
88
|
`ActivityContent` form of `request` is iOS 16.2+.
|
|
116
89
|
|
|
117
|
-
|
|
118
|
-
func beginCharging(at station: String) {
|
|
119
|
-
let attributes = ChargingAttributes(stationName: station, connectorLabel: "B2")
|
|
120
|
-
let window = Date.now...Date.now.addingTimeInterval(30 * 60)
|
|
121
|
-
let initial = ChargingAttributes.ContentState(
|
|
122
|
-
batteryPercent: 18, finishWindow: window, phase: .connecting
|
|
123
|
-
)
|
|
124
|
-
do {
|
|
125
|
-
let activity = try Activity.request(
|
|
126
|
-
attributes: attributes,
|
|
127
|
-
content: ActivityContent(state: initial, staleDate: nil, relevanceScore: 75),
|
|
128
|
-
pushType: .token
|
|
129
|
-
)
|
|
130
|
-
sessionStore.remember(activity.id)
|
|
131
|
-
} catch {
|
|
132
|
-
logger.error("Live Activity refused: \(error.localizedDescription)")
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
```
|
|
90
|
+
Code: [Start](references/code-examples.md#start).
|
|
136
91
|
|
|
137
92
|
### Update
|
|
138
93
|
|
|
@@ -198,49 +153,7 @@ The Lock Screen is the surface that always exists: every device on iOS 16.1 or
|
|
|
198
153
|
later shows Live Activities there. Lay it out first, then squeeze it into the
|
|
199
154
|
Dynamic Island.
|
|
200
155
|
|
|
201
|
-
|
|
202
|
-
import SwiftUI
|
|
203
|
-
import WidgetKit
|
|
204
|
-
|
|
205
|
-
struct ChargingLiveActivity: Widget {
|
|
206
|
-
var body: some WidgetConfiguration {
|
|
207
|
-
ActivityConfiguration(for: ChargingAttributes.self) { context in
|
|
208
|
-
HStack(spacing: 12) {
|
|
209
|
-
Image(systemName: context.state.phase.symbolName)
|
|
210
|
-
VStack(alignment: .leading) {
|
|
211
|
-
Text(context.attributes.stationName).font(.headline)
|
|
212
|
-
Text("\(context.state.batteryPercent)%")
|
|
213
|
-
}
|
|
214
|
-
Spacer()
|
|
215
|
-
if context.isStale {
|
|
216
|
-
Label("Refreshing", systemImage: "arrow.trianglehead.2.clockwise")
|
|
217
|
-
.foregroundStyle(.secondary)
|
|
218
|
-
} else {
|
|
219
|
-
Text(timerInterval: context.state.finishWindow, countsDown: true)
|
|
220
|
-
.monospacedDigit()
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
.padding()
|
|
224
|
-
} dynamicIsland: { context in
|
|
225
|
-
DynamicIsland {
|
|
226
|
-
DynamicIslandExpandedRegion(.center) {
|
|
227
|
-
Text(context.attributes.stationName)
|
|
228
|
-
}
|
|
229
|
-
DynamicIslandExpandedRegion(.trailing) {
|
|
230
|
-
Text("\(context.state.batteryPercent)%")
|
|
231
|
-
}
|
|
232
|
-
} compactLeading: {
|
|
233
|
-
Image(systemName: "bolt.car")
|
|
234
|
-
} compactTrailing: {
|
|
235
|
-
Text(timerInterval: context.state.finishWindow, countsDown: true)
|
|
236
|
-
.monospacedDigit()
|
|
237
|
-
} minimal: {
|
|
238
|
-
Image(systemName: "bolt.car")
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
```
|
|
156
|
+
Code: [Lock Screen](references/code-examples.md#lock-screen).
|
|
244
157
|
|
|
245
158
|
Static values come from `context.attributes`; changing values from
|
|
246
159
|
`context.state`.
|
|
@@ -447,3 +360,4 @@ update or end activities; they cannot start one. They are addressed with
|
|
|
447
360
|
attributes model, start/update/end variants, server payloads and headers, channel pushes, concurrent activities, state and token observation,
|
|
448
361
|
authorization, errors, background behaviour, testing, Info.plist keys and
|
|
449
362
|
documentation links.
|
|
363
|
+
- [Code examples](references/code-examples.md): open for the full code behind Modelling the data, Start, Lock Screen.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Live Activities Code Examples
|
|
2
|
+
|
|
3
|
+
<!-- toc -->
|
|
4
|
+
- [Modelling the data](#modelling-the-data)
|
|
5
|
+
- [Start](#start)
|
|
6
|
+
- [Lock Screen](#lock-screen)
|
|
7
|
+
<!-- /toc -->
|
|
8
|
+
|
|
9
|
+
Full code samples for the SKILL.md sections that link here. The rules and the decision guidance stay in SKILL.md; each heading below matches the SKILL.md section the code belongs to.
|
|
10
|
+
|
|
11
|
+
## Modelling the data
|
|
12
|
+
|
|
13
|
+
```swift
|
|
14
|
+
import ActivityKit
|
|
15
|
+
|
|
16
|
+
struct ChargingAttributes: ActivityAttributes {
|
|
17
|
+
struct ContentState: Codable, Hashable {
|
|
18
|
+
var batteryPercent: Int
|
|
19
|
+
var finishWindow: ClosedRange<Date>
|
|
20
|
+
var phase: ChargePhase
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
var stationName: String
|
|
24
|
+
var connectorLabel: String
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
enum ChargePhase: String, Codable, Hashable, CaseIterable {
|
|
28
|
+
case queued, connecting, charging, balancing, complete
|
|
29
|
+
|
|
30
|
+
var symbolName: String {
|
|
31
|
+
switch self {
|
|
32
|
+
case .queued: "hourglass"
|
|
33
|
+
case .connecting: "powerplug"
|
|
34
|
+
case .charging: "bolt.car"
|
|
35
|
+
case .balancing: "slider.horizontal.3"
|
|
36
|
+
case .complete: "checkmark.circle.fill"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Start
|
|
43
|
+
|
|
44
|
+
```swift
|
|
45
|
+
func beginCharging(at station: String) {
|
|
46
|
+
let attributes = ChargingAttributes(stationName: station, connectorLabel: "B2")
|
|
47
|
+
let window = Date.now...Date.now.addingTimeInterval(30 * 60)
|
|
48
|
+
let initial = ChargingAttributes.ContentState(
|
|
49
|
+
batteryPercent: 18, finishWindow: window, phase: .connecting
|
|
50
|
+
)
|
|
51
|
+
do {
|
|
52
|
+
let activity = try Activity.request(
|
|
53
|
+
attributes: attributes,
|
|
54
|
+
content: ActivityContent(state: initial, staleDate: nil, relevanceScore: 75),
|
|
55
|
+
pushType: .token
|
|
56
|
+
)
|
|
57
|
+
sessionStore.remember(activity.id)
|
|
58
|
+
} catch {
|
|
59
|
+
logger.error("Live Activity refused: \(error.localizedDescription)")
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Lock Screen
|
|
65
|
+
|
|
66
|
+
```swift
|
|
67
|
+
import SwiftUI
|
|
68
|
+
import WidgetKit
|
|
69
|
+
|
|
70
|
+
struct ChargingLiveActivity: Widget {
|
|
71
|
+
var body: some WidgetConfiguration {
|
|
72
|
+
ActivityConfiguration(for: ChargingAttributes.self) { context in
|
|
73
|
+
HStack(spacing: 12) {
|
|
74
|
+
Image(systemName: context.state.phase.symbolName)
|
|
75
|
+
VStack(alignment: .leading) {
|
|
76
|
+
Text(context.attributes.stationName).font(.headline)
|
|
77
|
+
Text("\(context.state.batteryPercent)%")
|
|
78
|
+
}
|
|
79
|
+
Spacer()
|
|
80
|
+
if context.isStale {
|
|
81
|
+
Label("Refreshing", systemImage: "arrow.trianglehead.2.clockwise")
|
|
82
|
+
.foregroundStyle(.secondary)
|
|
83
|
+
} else {
|
|
84
|
+
Text(timerInterval: context.state.finishWindow, countsDown: true)
|
|
85
|
+
.monospacedDigit()
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
.padding()
|
|
89
|
+
} dynamicIsland: { context in
|
|
90
|
+
DynamicIsland {
|
|
91
|
+
DynamicIslandExpandedRegion(.center) {
|
|
92
|
+
Text(context.attributes.stationName)
|
|
93
|
+
}
|
|
94
|
+
DynamicIslandExpandedRegion(.trailing) {
|
|
95
|
+
Text("\(context.state.batteryPercent)%")
|
|
96
|
+
}
|
|
97
|
+
} compactLeading: {
|
|
98
|
+
Image(systemName: "bolt.car")
|
|
99
|
+
} compactTrailing: {
|
|
100
|
+
Text(timerInterval: context.state.finishWindow, countsDown: true)
|
|
101
|
+
.monospacedDigit()
|
|
102
|
+
} minimal: {
|
|
103
|
+
Image(systemName: "bolt.car")
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
@@ -25,30 +25,15 @@ It is not a key-minting tool: new keys and their values belong to the resource-a
|
|
|
25
25
|
|
|
26
26
|
## Project settings
|
|
27
27
|
|
|
28
|
-
Nothing company-specific is built in. Establish these once per project
|
|
29
|
-
|
|
30
|
-
| Setting | What it is | Example |
|
|
31
|
-
|---|---|---|
|
|
32
|
-
| Specs repo + design export | Screen frames, `components-used.md`, `screenshot.png`, `tree.json` | `design-export/<project>/screens/<slug>/` |
|
|
33
|
-
| Resources root | Suggested new values and the component registry | `resources/Localization/Suggested/<Key>.json` |
|
|
34
|
-
| Legacy label endpoint | For refreshing the legacy snapshot | `--endpoint` + `--headers-file` |
|
|
35
|
-
| Legacy key prefix | Display-only prefix the backend stores | mapping `legacyKeyPrefix`, e.g. `App-` |
|
|
36
|
-
| Web i18n convention | For legacy and/or new web apps | `t('key')` / `$t('key')`, keys in `locales/<lang>.json` |
|
|
37
|
-
| Confluence target | Base URL, space, parent page | `CONFLUENCE_BASE_URL` / `CONFLUENCE_SPACE` / `CONFLUENCE_PARENT` |
|
|
38
|
-
| CMS taxonomy | Property Group / Module buckets in the spreadsheet | `--taxonomy cms-taxonomy.json` |
|
|
39
|
-
| Document language | Headings, legend and labels | `--ui-lang en` (default) or `tr` |
|
|
40
|
-
|
|
41
|
-
Secrets live only in a keychain item or a `--headers-file`. They never go into the mapping, the taxonomy file, the docs or a command line.
|
|
42
|
-
|
|
43
|
-
At the start, ask the user only for what is still unknown: where the implemented screen lives, the Figma file key and node-ids (or a confirmed design-export slug), and, once it is time to publish, the Confluence target and the formats wanted.
|
|
28
|
+
Nothing company-specific is built in. Establish these once per project and reuse them for every screen: specs repo and design export, resources root, legacy label endpoint, legacy key prefix, web i18n convention, Confluence target, CMS taxonomy and document language. Secrets live only in a keychain item or a `--headers-file`, never in the mapping, the taxonomy file, the docs or a command line. Ask the user only for what is still unknown. Table with examples: [Project settings](references/project-settings.md).
|
|
44
29
|
|
|
45
30
|
## Where this sits
|
|
46
31
|
|
|
47
|
-
After the screen spec exists: screen spec, then this map, then publish. If the repo holds a snapshot, the registry and the Suggested files are taken from it (see [publish-and-snapshot](
|
|
32
|
+
After the screen spec exists: screen spec, then this map, then publish. If the repo holds a snapshot, the registry and the Suggested files are taken from it (see [publish-and-snapshot](references/publish-and-snapshot.md)).
|
|
48
33
|
|
|
49
34
|
## Steps
|
|
50
35
|
|
|
51
|
-
The helpers in `scripts/` use nothing beyond the Python 3 standard library, apart from one bash script. Commands and how each source is traced are covered in [sources-and-recipes](
|
|
36
|
+
The helpers in `scripts/` use nothing beyond the Python 3 standard library, apart from one bash script. Commands and how each source is traced are covered in [sources-and-recipes](references/sources-and-recipes.md).
|
|
52
37
|
|
|
53
38
|
1. **Enumerate new keys.** Take the union of three sources: the design export's `components-used.md` across every state frame, each component's registry `localizationKeys`, and the keys the screen code wires. Tag each key screen-specific, component-owned or shared. Every key is confirmed by comparing sources, never lifted from one of them.
|
|
54
39
|
2. **Catch what the cross-reference misses.** First find the type the project generates its keys into (for example `grep -rhoE '\b[A-Z][A-Za-z0-9_]*\.[A-Z][A-Za-z0-9_]*\.[A-Za-z_]+' <dir> | cut -d. -f1 | sort | uniq -c | sort -rn`) and set it as `"keyType"` in the mapping; the built-in default `L10n` is only a fallback, and a project that uses another accessor gets its real keys missed or downgraded to weak. Then run `scan-screen-keys.py --screen-path <dir> --key-type <KeyType>`. A `WARNING: no key chain of ...` line on stderr means the type is wrong: fix it before going on. Each `error` and `dynamic` hit becomes a mapping row; compare the `static` hits with the union from step 1.
|
|
@@ -56,13 +41,13 @@ The helpers in `scripts/` use nothing beyond the Python 3 standard library, apar
|
|
|
56
41
|
4. **CMS copy.** Run `fetch-annotations.py --mapping <map>.json [--local <export-screen-dir>]`: it reads REST and falls back to `--local` when REST fails. When REST is rate-limited or has no token, dump the annotations through Figma MCP and run it again with `--from-mcp <dump>.json`. Fill `cms` and `cmsNodeId` on each row, and send any TR without EN back to the content team.
|
|
57
42
|
5. **Trace legacy keys** per platform: spec, then view controller, view model and cell presentation models on iOS; layout `@string/` on Android; call sites on web.
|
|
58
43
|
6. **Legacy values.** Run `resolve-legacy-values.py --keys "<legacy>" --snapshot-root <res>/Localization/Legacy --langs all`; if the snapshot is out of date, `fetch-legacy-labels.py` refreshes it. A value that cannot be found stays blank and gets a note.
|
|
59
|
-
7. **Assemble the mapping JSON**: one row per element, plus `screenshot`, `figmaFileKey` and `legacyKeyPrefix` where they apply. Schema: [format-and-output](
|
|
44
|
+
7. **Assemble the mapping JSON**: one row per element, plus `screenshot`, `figmaFileKey` and `legacyKeyPrefix` where they apply. Schema: [format-and-output](references/format-and-output.md).
|
|
60
45
|
8. **Overlay.** Run `render-overlay.py --out <dir> --mapping <map>.json`; it produces `<slug>.overlay.png`, which the mapping's `overlay` field then names.
|
|
61
46
|
9. **Key screenshots.** Run `render-key-shots.py --out <dir> --mapping <map>.json [--slug]`; it fills `keyshots/` and writes the manifest, which the mapping's `keyshots` field then names.
|
|
62
47
|
10. **Artifacts.** `build-artifact.py <map>.json --out <dir>` (add `--docx`, `--pdf` or `--all` on request).
|
|
63
48
|
11. **Spreadsheet.** `build-spreadsheet.py <map>.json --out <dir>` (add `--csv` or `--csv-only`).
|
|
64
49
|
12. **Gate.** Run `verify-map.py --screen-path <dir> --mapping <map>.json [--resources-root <res>]`, with the accessor from step 2 in the mapping's `keyType` (or passed as `--key-type <KeyType>`). Exit code 2 stops publishing. A PASS that carries the key type `WARNING` is not a pass: set the accessor and run it again.
|
|
65
|
-
13. **Publish.** Ask which target(s): (A) Confluence via `publish-confluence.py`, (B) a specs-repo PR under `localizations/<slug>/`, or both. Details: [publish-and-snapshot](
|
|
50
|
+
13. **Publish.** Ask which target(s): (A) Confluence via `publish-confluence.py`, (B) a specs-repo PR under `localizations/<slug>/`, or both. Details: [publish-and-snapshot](references/publish-and-snapshot.md).
|
|
66
51
|
|
|
67
52
|
When the Figma frame carries no annotations, say so, explain that keys will be mapped by reading the screen, and confirm the screen state (mock or screenshot) and the anchored element list with the user before rendering.
|
|
68
53
|
|
|
@@ -96,7 +81,8 @@ Edge cases:
|
|
|
96
81
|
|
|
97
82
|
## References
|
|
98
83
|
|
|
99
|
-
- [
|
|
100
|
-
- [
|
|
101
|
-
- [
|
|
84
|
+
- [Project settings](references/project-settings.md): the per-project settings table with examples, and the questions to ask at the start.
|
|
85
|
+
- [sources-and-recipes](references/sources-and-recipes.md): the origin of every column, legacy tracing on each platform, overlay and keyshot geometry, pitfalls.
|
|
86
|
+
- [format-and-output](references/format-and-output.md): the mapping schema, artifact formats, spreadsheet columns and the script reference.
|
|
87
|
+
- [publish-and-snapshot](references/publish-and-snapshot.md): the snapshot that keeps reads in one repo, Confluence publishing, and the specs-repo PR.
|
|
102
88
|
- [example mapping](example-mapping.json): a neutral mapping of nine rows that covers each edge case listed above.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Project settings
|
|
2
|
+
|
|
3
|
+
The per-project settings this skill needs before mapping any screen, with an example for each, and what to ask the user at the start.
|
|
4
|
+
|
|
5
|
+
## Project settings
|
|
6
|
+
|
|
7
|
+
Nothing company-specific is built in. Establish these once per project (read them from the project's docs or ask), then reuse them for every screen:
|
|
8
|
+
|
|
9
|
+
| Setting | What it is | Example |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| Specs repo + design export | Screen frames, `components-used.md`, `screenshot.png`, `tree.json` | `design-export/<project>/screens/<slug>/` |
|
|
12
|
+
| Resources root | Suggested new values and the component registry | `resources/Localization/Suggested/<Key>.json` |
|
|
13
|
+
| Legacy label endpoint | For refreshing the legacy snapshot | `--endpoint` + `--headers-file` |
|
|
14
|
+
| Legacy key prefix | Display-only prefix the backend stores | mapping `legacyKeyPrefix`, e.g. `App-` |
|
|
15
|
+
| Web i18n convention | For legacy and/or new web apps | `t('key')` / `$t('key')`, keys in `locales/<lang>.json` |
|
|
16
|
+
| Confluence target | Base URL, space, parent page | `CONFLUENCE_BASE_URL` / `CONFLUENCE_SPACE` / `CONFLUENCE_PARENT` |
|
|
17
|
+
| CMS taxonomy | Property Group / Module buckets in the spreadsheet | `--taxonomy cms-taxonomy.json` |
|
|
18
|
+
| Document language | Headings, legend and labels | `--ui-lang en` (default) or `tr` |
|
|
19
|
+
|
|
20
|
+
Secrets live only in a keychain item or a `--headers-file`. They never go into the mapping, the taxonomy file, the docs or a command line.
|
|
21
|
+
|
|
22
|
+
At the start, ask the user only for what is still unknown: where the implemented screen lives, the Figma file key and node-ids (or a confirmed design-export slug), and, once it is time to publish, the Confluence target and the formats wanted.
|
|
@@ -90,30 +90,7 @@ Go through the [Review Checklist](#review-checklist) below.
|
|
|
90
90
|
|
|
91
91
|
A SwiftUI map file imports both `SwiftUI` and `MapKit`.
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
import SwiftUI
|
|
95
|
-
import MapKit
|
|
96
|
-
|
|
97
|
-
struct TrailheadMap: View {
|
|
98
|
-
@State private var camera: MapCameraPosition = .automatic
|
|
99
|
-
|
|
100
|
-
private let lot = CLLocationCoordinate2D(latitude: 46.852, longitude: -121.760)
|
|
101
|
-
private let summit = CLLocationCoordinate2D(latitude: 46.853, longitude: -121.727)
|
|
102
|
-
|
|
103
|
-
var body: some View {
|
|
104
|
-
Map(position: $camera) {
|
|
105
|
-
Marker("Parking", coordinate: lot)
|
|
106
|
-
Marker("Summit", systemImage: "mountain.2.fill", coordinate: summit)
|
|
107
|
-
}
|
|
108
|
-
.mapStyle(.standard(elevation: .realistic))
|
|
109
|
-
.mapControls {
|
|
110
|
-
MapUserLocationButton()
|
|
111
|
-
MapCompass()
|
|
112
|
-
MapScaleView()
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
```
|
|
93
|
+
Code: [SwiftUI Map (iOS 17+)](references/code-examples.md#swiftui-map-ios-17).
|
|
117
94
|
|
|
118
95
|
### Marker or Annotation
|
|
119
96
|
|
|
@@ -174,26 +151,7 @@ region are in [Camera Control](references/mapkit-patterns.md#camera-control).
|
|
|
174
151
|
The value in `.tag(_:)` must have the same type as the `selection` binding,
|
|
175
152
|
or taps select nothing.
|
|
176
153
|
|
|
177
|
-
|
|
178
|
-
struct CampsiteMap: View {
|
|
179
|
-
let sites: [Campsite]
|
|
180
|
-
@State private var chosen: MKMapItem?
|
|
181
|
-
@State private var headline = ""
|
|
182
|
-
|
|
183
|
-
var body: some View {
|
|
184
|
-
Map(selection: $chosen) {
|
|
185
|
-
ForEach(sites) { site in
|
|
186
|
-
Marker(site.name, coordinate: site.coordinate)
|
|
187
|
-
.tag(site.mapItem)
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
.onChange(of: chosen) { _, item in
|
|
191
|
-
guard let item else { return }
|
|
192
|
-
headline = item.name ?? ""
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
```
|
|
154
|
+
Code: [Selection](references/code-examples.md#selection).
|
|
197
155
|
|
|
198
156
|
## CoreLocation
|
|
199
157
|
|
|
@@ -210,39 +168,7 @@ element is a `CLLocationUpdate` whose `location` is optional. Rules:
|
|
|
210
168
|
sufficiently in use). React with a visible fallback instead of waiting on a
|
|
211
169
|
sequence that will not deliver.
|
|
212
170
|
|
|
213
|
-
|
|
214
|
-
import CoreLocation
|
|
215
|
-
import Observation
|
|
216
|
-
|
|
217
|
-
@MainActor
|
|
218
|
-
@Observable
|
|
219
|
-
final class RunTracker {
|
|
220
|
-
private(set) var lastFix: CLLocation?
|
|
221
|
-
private var stream: Task<Void, Never>?
|
|
222
|
-
|
|
223
|
-
func begin() {
|
|
224
|
-
stream?.cancel()
|
|
225
|
-
stream = Task {
|
|
226
|
-
do {
|
|
227
|
-
for try await reading in CLLocationUpdate.liveUpdates() {
|
|
228
|
-
guard let fix = reading.location,
|
|
229
|
-
fix.horizontalAccuracy >= 0,
|
|
230
|
-
fix.horizontalAccuracy < 50 else { continue }
|
|
231
|
-
lastFix = fix
|
|
232
|
-
}
|
|
233
|
-
} catch is CancellationError {
|
|
234
|
-
} catch {
|
|
235
|
-
lastFix = nil
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
func end() {
|
|
241
|
-
stream?.cancel()
|
|
242
|
-
stream = nil
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
```
|
|
171
|
+
Code: [CLLocationUpdate.liveUpdates() (iOS 17+)](references/code-examples.md#cllocationupdateliveupdates-ios-17).
|
|
246
172
|
|
|
247
173
|
Diagnostics, the full filter set and a mapping from each state to UI are in
|
|
248
174
|
the [location reference](references/mapkit-corelocation-patterns.md#diagnostics-and-fallbacks-ios-18).
|
|
@@ -271,24 +197,7 @@ let nearbySession = CLServiceSession(
|
|
|
271
197
|
|
|
272
198
|
A denied user should see why the feature is empty and a way out:
|
|
273
199
|
|
|
274
|
-
|
|
275
|
-
struct LocationOffView: View {
|
|
276
|
-
@Environment(\.openURL) private var openURL
|
|
277
|
-
|
|
278
|
-
var body: some View {
|
|
279
|
-
ContentUnavailableView {
|
|
280
|
-
Label("Location is off", systemImage: "location.slash")
|
|
281
|
-
} description: {
|
|
282
|
-
Text("Allow location access to see trails near you.")
|
|
283
|
-
} actions: {
|
|
284
|
-
Button("Open Settings") {
|
|
285
|
-
guard let settings = URL(string: UIApplication.openSettingsURLString) else { return }
|
|
286
|
-
openURL(settings)
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
```
|
|
200
|
+
Code: [Authorization](references/code-examples.md#authorization).
|
|
292
201
|
|
|
293
202
|
## Geocoding
|
|
294
203
|
|
|
@@ -459,3 +368,4 @@ func mapItem(near point: CLLocationCoordinate2D, named name: String) async throw
|
|
|
459
368
|
- [Location patterns](references/mapkit-corelocation-patterns.md):
|
|
460
369
|
`CLLocationUpdate`, `CLServiceSession`, `CLMonitor`, background location,
|
|
461
370
|
testing, privacy keys.
|
|
371
|
+
- [Code examples](references/code-examples.md): open for the full code behind SwiftUI Map (iOS 17+), Selection, CLLocationUpdate.liveUpdates() (iOS 17+), Authorization.
|