@mmerterden/multi-agent-pipeline 20.36.0 → 20.37.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 +12 -0
- package/docs/facts.json +1 -1
- package/install/catalog-history.json +1 -1
- package/manifest.json +194 -122
- 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/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/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/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/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/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/schemas/runs-index.schema.json +2 -1
- package/pipeline/scripts/contract-server.mjs +1 -1
- package/pipeline/scripts/runs-index.mjs +34 -7
- package/pipeline/scripts/status-overview.mjs +401 -0
- package/pipeline/skills/.skill-manifest.json +63 -63
- 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-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
|
@@ -61,36 +61,11 @@ be computed some other way.
|
|
|
61
61
|
Write `init(from:)` and `encode(to:)` when a value needs converting on the
|
|
62
62
|
way in or out.
|
|
63
63
|
|
|
64
|
-
```swift
|
|
65
|
-
struct Shipment: Codable {
|
|
66
|
-
let reference: String
|
|
67
|
-
let dispatchedAt: Date
|
|
68
|
-
let labels: [String]
|
|
69
|
-
|
|
70
|
-
enum CodingKeys: String, CodingKey {
|
|
71
|
-
case reference, dispatchedAt = "dispatched_epoch", labels
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
init(from decoder: any Decoder) throws {
|
|
75
|
-
let fields = try decoder.container(keyedBy: CodingKeys.self)
|
|
76
|
-
reference = try fields.decode(String.self, forKey: .reference)
|
|
77
|
-
let seconds = try fields.decode(Double.self, forKey: .dispatchedAt)
|
|
78
|
-
dispatchedAt = Date(timeIntervalSince1970: seconds)
|
|
79
|
-
labels = try fields.decodeIfPresent([String].self, forKey: .labels) ?? []
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
func encode(to encoder: any Encoder) throws {
|
|
83
|
-
var fields = encoder.container(keyedBy: CodingKeys.self)
|
|
84
|
-
try fields.encode(reference, forKey: .reference)
|
|
85
|
-
try fields.encode(dispatchedAt.timeIntervalSince1970, forKey: .dispatchedAt)
|
|
86
|
-
try fields.encode(labels, forKey: .labels)
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
```
|
|
90
|
-
|
|
91
64
|
Doing the conversion here, instead of patching values after decoding, keeps
|
|
92
65
|
invalid instances from ever existing.
|
|
93
66
|
|
|
67
|
+
Code for this section: [Codable recipes](references/codable-recipes.md).
|
|
68
|
+
|
|
94
69
|
## Nested and flattened containers
|
|
95
70
|
|
|
96
71
|
JSON often nests what the app wants flat. `nestedContainer(keyedBy:forKey:)`
|
|
@@ -125,65 +100,21 @@ For a list whose elements differ by a `kind` field, model the element as an
|
|
|
125
100
|
enum with associated values. Read the discriminator first, then the payload it
|
|
126
101
|
selects.
|
|
127
102
|
|
|
128
|
-
|
|
129
|
-
enum FeedItem: Decodable {
|
|
130
|
-
case note(String)
|
|
131
|
-
case photo(URL)
|
|
132
|
-
case poll(options: [String])
|
|
133
|
-
|
|
134
|
-
enum Keys: String, CodingKey { case kind, text, url, options }
|
|
135
|
-
|
|
136
|
-
init(from decoder: any Decoder) throws {
|
|
137
|
-
let item = try decoder.container(keyedBy: Keys.self)
|
|
138
|
-
switch try item.decode(String.self, forKey: .kind) {
|
|
139
|
-
case "note": self = .note(try item.decode(String.self, forKey: .text))
|
|
140
|
-
case "photo": self = .photo(try item.decode(URL.self, forKey: .url))
|
|
141
|
-
case "poll": self = .poll(options: try item.decode([String].self, forKey: .options))
|
|
142
|
-
case let other:
|
|
143
|
-
throw DecodingError.dataCorruptedError(
|
|
144
|
-
forKey: .kind, in: item, debugDescription: "No FeedItem kind named \(other)")
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
let feed = try JSONDecoder().decode([FeedItem].self, from: payload)
|
|
150
|
-
```
|
|
103
|
+
Code for this section: [Codable recipes](references/codable-recipes.md).
|
|
151
104
|
|
|
152
105
|
## Date decoding strategies
|
|
153
106
|
|
|
154
|
-
Set the strategy on the decoder to match what the server sends
|
|
155
|
-
|
|
156
|
-
```swift
|
|
157
|
-
let reader = JSONDecoder()
|
|
158
|
-
|
|
159
|
-
reader.dateDecodingStrategy = .iso8601 // "2025-11-02T08:15:00Z"
|
|
160
|
-
reader.dateDecodingStrategy = .secondsSince1970 // 1762071300
|
|
161
|
-
|
|
162
|
-
let dayFormat = DateFormatter()
|
|
163
|
-
dayFormat.dateFormat = "dd.MM.yyyy"
|
|
164
|
-
dayFormat.locale = Locale(identifier: "en_US_POSIX")
|
|
165
|
-
dayFormat.timeZone = TimeZone(secondsFromGMT: 0)
|
|
166
|
-
reader.dateDecodingStrategy = .formatted(dayFormat) // "02.11.2025"
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
When one field arrives in more than one shape, decode it by hand:
|
|
107
|
+
Set the strategy on the decoder to match what the server sends: `.iso8601`,
|
|
108
|
+
`.secondsSince1970`, or `.formatted` with an `en_US_POSIX`, UTC `DateFormatter`.
|
|
170
109
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
let raw = try decoder.singleValueContainer()
|
|
174
|
-
let text = try raw.decode(String.self)
|
|
175
|
-
let withFraction = ISO8601DateFormatter()
|
|
176
|
-
withFraction.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
|
177
|
-
if let date = withFraction.date(from: text) ?? ISO8601DateFormatter().date(from: text) {
|
|
178
|
-
return date
|
|
179
|
-
}
|
|
180
|
-
throw DecodingError.dataCorruptedError(in: raw, debugDescription: "Unreadable date: \(text)")
|
|
181
|
-
}
|
|
182
|
-
```
|
|
110
|
+
When one field arrives in more than one shape, decode it by hand with a
|
|
111
|
+
`.custom` strategy.
|
|
183
112
|
|
|
184
113
|
Give the encoder the matching strategy, for example
|
|
185
114
|
`writer.dateEncodingStrategy = .iso8601`, so values round-trip.
|
|
186
115
|
|
|
116
|
+
Code for this section: [Codable recipes](references/codable-recipes.md).
|
|
117
|
+
|
|
187
118
|
## Data and key strategies
|
|
188
119
|
|
|
189
120
|
```swift
|
|
@@ -211,37 +142,15 @@ ordinary key.
|
|
|
211
142
|
|
|
212
143
|
By default a single bad element throws, and the whole array is lost. When a
|
|
213
144
|
feed is known to be unreliable, wrap each element so a failure becomes `nil`
|
|
214
|
-
and the container still moves on
|
|
215
|
-
|
|
216
|
-
```swift
|
|
217
|
-
struct TolerantList<Element: Decodable>: Decodable {
|
|
218
|
-
let items: [Element]
|
|
219
|
-
|
|
220
|
-
private struct Slot: Decodable {
|
|
221
|
-
let value: Element?
|
|
222
|
-
init(from decoder: any Decoder) throws {
|
|
223
|
-
value = try? Element(from: decoder)
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
init(from decoder: any Decoder) throws {
|
|
228
|
-
var list = try decoder.unkeyedContainer()
|
|
229
|
-
var kept: [Element] = []
|
|
230
|
-
while !list.isAtEnd {
|
|
231
|
-
if let value = try list.decode(Slot.self).value {
|
|
232
|
-
kept.append(value)
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
items = kept
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
```
|
|
145
|
+
and the container still moves on (the `Slot` wrapper in the reference).
|
|
239
146
|
|
|
240
147
|
Decoding `Slot` always succeeds, so the unkeyed container always advances.
|
|
241
148
|
A skip trick that decodes an empty placeholder struct only works when the bad
|
|
242
149
|
element is a JSON object; a bad string or number would throw again and leave
|
|
243
150
|
the loop stuck on the same index.
|
|
244
151
|
|
|
152
|
+
Code for this section: [Codable recipes](references/codable-recipes.md).
|
|
153
|
+
|
|
245
154
|
## Single value containers
|
|
246
155
|
|
|
247
156
|
A wrapper type that should appear in JSON as a bare value uses the single value
|
|
@@ -394,52 +303,8 @@ needs something SwiftData cannot store, hand it to the SwiftData skill.
|
|
|
394
303
|
|
|
395
304
|
`@AppStorage` is for small preferences: `Bool`, numbers, `String`, `URL`,
|
|
396
305
|
`Data`, or a `RawRepresentable` whose raw value is `Int` or `String`. A small
|
|
397
|
-
Codable preference can ride on a JSON string raw value
|
|
398
|
-
|
|
399
|
-
```swift
|
|
400
|
-
struct PlaybackPrefs: Codable {
|
|
401
|
-
var speed: Double = 1.0
|
|
402
|
-
var skipSilence = false
|
|
403
|
-
|
|
404
|
-
init() {}
|
|
405
|
-
|
|
406
|
-
enum CodingKeys: String, CodingKey { case speed, skipSilence }
|
|
407
|
-
|
|
408
|
-
init(from decoder: any Decoder) throws {
|
|
409
|
-
let fields = try decoder.container(keyedBy: CodingKeys.self)
|
|
410
|
-
speed = try fields.decodeIfPresent(Double.self, forKey: .speed) ?? 1.0
|
|
411
|
-
skipSilence = try fields.decodeIfPresent(Bool.self, forKey: .skipSilence) ?? false
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
func encode(to encoder: any Encoder) throws {
|
|
415
|
-
var fields = encoder.container(keyedBy: CodingKeys.self)
|
|
416
|
-
try fields.encode(speed, forKey: .speed)
|
|
417
|
-
try fields.encode(skipSilence, forKey: .skipSilence)
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
extension PlaybackPrefs: RawRepresentable {
|
|
422
|
-
init?(rawValue: String) {
|
|
423
|
-
guard let bytes = rawValue.data(using: .utf8),
|
|
424
|
-
let value = try? JSONDecoder().decode(PlaybackPrefs.self, from: bytes) else { return nil }
|
|
425
|
-
self = value
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
var rawValue: String {
|
|
429
|
-
guard let bytes = try? JSONEncoder().encode(self),
|
|
430
|
-
let text = String(data: bytes, encoding: .utf8) else { return "{}" }
|
|
431
|
-
return text
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
struct PlaybackSettingsView: View {
|
|
436
|
-
@AppStorage("playbackPrefs") private var prefs = PlaybackPrefs()
|
|
437
|
-
|
|
438
|
-
var body: some View {
|
|
439
|
-
Toggle("Skip silence", isOn: $prefs.skipSilence)
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
```
|
|
306
|
+
Codable preference can ride on a JSON string raw value (full type in the
|
|
307
|
+
reference).
|
|
443
308
|
|
|
444
309
|
Two details make this bridge safe:
|
|
445
310
|
|
|
@@ -451,6 +316,8 @@ Two details make this bridge safe:
|
|
|
451
316
|
values stored by an older version with fewer keys still decode. Synthesized
|
|
452
317
|
decoding would ignore the property defaults and fail on `{}`.
|
|
453
318
|
|
|
319
|
+
Code for this section: [Codable recipes](references/codable-recipes.md).
|
|
320
|
+
|
|
454
321
|
## Common mistakes
|
|
455
322
|
|
|
456
323
|
1. **`decode` for a key that may be absent.** It throws `keyNotFound`. Use
|
|
@@ -482,6 +349,7 @@ Two details make this bridge safe:
|
|
|
482
349
|
|
|
483
350
|
## References
|
|
484
351
|
|
|
352
|
+
- [Codable recipes](references/codable-recipes.md): open when writing a custom `init(from:)`, decoding mixed-type arrays, choosing a date strategy, skipping bad array elements or persisting Codable values in UserDefaults.
|
|
485
353
|
- [Codable](https://developer.apple.com/documentation/swift/codable)
|
|
486
354
|
- [JSONDecoder](https://developer.apple.com/documentation/foundation/jsondecoder)
|
|
487
355
|
- [JSONEncoder](https://developer.apple.com/documentation/foundation/jsonencoder)
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# Codable recipes
|
|
2
|
+
|
|
3
|
+
<!-- toc -->
|
|
4
|
+
- [Custom decoding and encoding](#custom-decoding-and-encoding)
|
|
5
|
+
- [Heterogeneous arrays](#heterogeneous-arrays)
|
|
6
|
+
- [Date decoding strategies](#date-decoding-strategies)
|
|
7
|
+
- [Lossy array decoding](#lossy-array-decoding)
|
|
8
|
+
- [Codable with UserDefaults](#codable-with-userdefaults)
|
|
9
|
+
<!-- /toc -->
|
|
10
|
+
|
|
11
|
+
Full code for custom decoding, heterogeneous arrays, date strategies, lossy arrays and storing Codable values in UserDefaults. SKILL.md keeps the rules and when to use each technique.
|
|
12
|
+
|
|
13
|
+
## Custom decoding and encoding
|
|
14
|
+
|
|
15
|
+
Write `init(from:)` and `encode(to:)` when a value needs converting on the
|
|
16
|
+
way in or out.
|
|
17
|
+
|
|
18
|
+
```swift
|
|
19
|
+
struct Shipment: Codable {
|
|
20
|
+
let reference: String
|
|
21
|
+
let dispatchedAt: Date
|
|
22
|
+
let labels: [String]
|
|
23
|
+
|
|
24
|
+
enum CodingKeys: String, CodingKey {
|
|
25
|
+
case reference, dispatchedAt = "dispatched_epoch", labels
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
init(from decoder: any Decoder) throws {
|
|
29
|
+
let fields = try decoder.container(keyedBy: CodingKeys.self)
|
|
30
|
+
reference = try fields.decode(String.self, forKey: .reference)
|
|
31
|
+
let seconds = try fields.decode(Double.self, forKey: .dispatchedAt)
|
|
32
|
+
dispatchedAt = Date(timeIntervalSince1970: seconds)
|
|
33
|
+
labels = try fields.decodeIfPresent([String].self, forKey: .labels) ?? []
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
func encode(to encoder: any Encoder) throws {
|
|
37
|
+
var fields = encoder.container(keyedBy: CodingKeys.self)
|
|
38
|
+
try fields.encode(reference, forKey: .reference)
|
|
39
|
+
try fields.encode(dispatchedAt.timeIntervalSince1970, forKey: .dispatchedAt)
|
|
40
|
+
try fields.encode(labels, forKey: .labels)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Doing the conversion here, instead of patching values after decoding, keeps
|
|
46
|
+
invalid instances from ever existing.
|
|
47
|
+
|
|
48
|
+
## Heterogeneous arrays
|
|
49
|
+
|
|
50
|
+
For a list whose elements differ by a `kind` field, model the element as an
|
|
51
|
+
enum with associated values. Read the discriminator first, then the payload it
|
|
52
|
+
selects.
|
|
53
|
+
|
|
54
|
+
```swift
|
|
55
|
+
enum FeedItem: Decodable {
|
|
56
|
+
case note(String)
|
|
57
|
+
case photo(URL)
|
|
58
|
+
case poll(options: [String])
|
|
59
|
+
|
|
60
|
+
enum Keys: String, CodingKey { case kind, text, url, options }
|
|
61
|
+
|
|
62
|
+
init(from decoder: any Decoder) throws {
|
|
63
|
+
let item = try decoder.container(keyedBy: Keys.self)
|
|
64
|
+
switch try item.decode(String.self, forKey: .kind) {
|
|
65
|
+
case "note": self = .note(try item.decode(String.self, forKey: .text))
|
|
66
|
+
case "photo": self = .photo(try item.decode(URL.self, forKey: .url))
|
|
67
|
+
case "poll": self = .poll(options: try item.decode([String].self, forKey: .options))
|
|
68
|
+
case let other:
|
|
69
|
+
throw DecodingError.dataCorruptedError(
|
|
70
|
+
forKey: .kind, in: item, debugDescription: "No FeedItem kind named \(other)")
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let feed = try JSONDecoder().decode([FeedItem].self, from: payload)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Date decoding strategies
|
|
79
|
+
|
|
80
|
+
Set the strategy on the decoder to match what the server sends.
|
|
81
|
+
|
|
82
|
+
```swift
|
|
83
|
+
let reader = JSONDecoder()
|
|
84
|
+
|
|
85
|
+
reader.dateDecodingStrategy = .iso8601 // "2025-11-02T08:15:00Z"
|
|
86
|
+
reader.dateDecodingStrategy = .secondsSince1970 // 1762071300
|
|
87
|
+
|
|
88
|
+
let dayFormat = DateFormatter()
|
|
89
|
+
dayFormat.dateFormat = "dd.MM.yyyy"
|
|
90
|
+
dayFormat.locale = Locale(identifier: "en_US_POSIX")
|
|
91
|
+
dayFormat.timeZone = TimeZone(secondsFromGMT: 0)
|
|
92
|
+
reader.dateDecodingStrategy = .formatted(dayFormat) // "02.11.2025"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
When one field arrives in more than one shape, decode it by hand:
|
|
96
|
+
|
|
97
|
+
```swift
|
|
98
|
+
reader.dateDecodingStrategy = .custom { decoder in
|
|
99
|
+
let raw = try decoder.singleValueContainer()
|
|
100
|
+
let text = try raw.decode(String.self)
|
|
101
|
+
let withFraction = ISO8601DateFormatter()
|
|
102
|
+
withFraction.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
|
103
|
+
if let date = withFraction.date(from: text) ?? ISO8601DateFormatter().date(from: text) {
|
|
104
|
+
return date
|
|
105
|
+
}
|
|
106
|
+
throw DecodingError.dataCorruptedError(in: raw, debugDescription: "Unreadable date: \(text)")
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Give the encoder the matching strategy, for example
|
|
111
|
+
`writer.dateEncodingStrategy = .iso8601`, so values round-trip.
|
|
112
|
+
|
|
113
|
+
## Lossy array decoding
|
|
114
|
+
|
|
115
|
+
By default a single bad element throws, and the whole array is lost. When a
|
|
116
|
+
feed is known to be unreliable, wrap each element so a failure becomes `nil`
|
|
117
|
+
and the container still moves on:
|
|
118
|
+
|
|
119
|
+
```swift
|
|
120
|
+
struct TolerantList<Element: Decodable>: Decodable {
|
|
121
|
+
let items: [Element]
|
|
122
|
+
|
|
123
|
+
private struct Slot: Decodable {
|
|
124
|
+
let value: Element?
|
|
125
|
+
init(from decoder: any Decoder) throws {
|
|
126
|
+
value = try? Element(from: decoder)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
init(from decoder: any Decoder) throws {
|
|
131
|
+
var list = try decoder.unkeyedContainer()
|
|
132
|
+
var kept: [Element] = []
|
|
133
|
+
while !list.isAtEnd {
|
|
134
|
+
if let value = try list.decode(Slot.self).value {
|
|
135
|
+
kept.append(value)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
items = kept
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Decoding `Slot` always succeeds, so the unkeyed container always advances.
|
|
144
|
+
A skip trick that decodes an empty placeholder struct only works when the bad
|
|
145
|
+
element is a JSON object; a bad string or number would throw again and leave
|
|
146
|
+
the loop stuck on the same index.
|
|
147
|
+
|
|
148
|
+
## Codable with UserDefaults
|
|
149
|
+
|
|
150
|
+
`@AppStorage` is for small preferences: `Bool`, numbers, `String`, `URL`,
|
|
151
|
+
`Data`, or a `RawRepresentable` whose raw value is `Int` or `String`. A small
|
|
152
|
+
Codable preference can ride on a JSON string raw value:
|
|
153
|
+
|
|
154
|
+
```swift
|
|
155
|
+
struct PlaybackPrefs: Codable {
|
|
156
|
+
var speed: Double = 1.0
|
|
157
|
+
var skipSilence = false
|
|
158
|
+
|
|
159
|
+
init() {}
|
|
160
|
+
|
|
161
|
+
enum CodingKeys: String, CodingKey { case speed, skipSilence }
|
|
162
|
+
|
|
163
|
+
init(from decoder: any Decoder) throws {
|
|
164
|
+
let fields = try decoder.container(keyedBy: CodingKeys.self)
|
|
165
|
+
speed = try fields.decodeIfPresent(Double.self, forKey: .speed) ?? 1.0
|
|
166
|
+
skipSilence = try fields.decodeIfPresent(Bool.self, forKey: .skipSilence) ?? false
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
func encode(to encoder: any Encoder) throws {
|
|
170
|
+
var fields = encoder.container(keyedBy: CodingKeys.self)
|
|
171
|
+
try fields.encode(speed, forKey: .speed)
|
|
172
|
+
try fields.encode(skipSilence, forKey: .skipSilence)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
extension PlaybackPrefs: RawRepresentable {
|
|
177
|
+
init?(rawValue: String) {
|
|
178
|
+
guard let bytes = rawValue.data(using: .utf8),
|
|
179
|
+
let value = try? JSONDecoder().decode(PlaybackPrefs.self, from: bytes) else { return nil }
|
|
180
|
+
self = value
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
var rawValue: String {
|
|
184
|
+
guard let bytes = try? JSONEncoder().encode(self),
|
|
185
|
+
let text = String(data: bytes, encoding: .utf8) else { return "{}" }
|
|
186
|
+
return text
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
struct PlaybackSettingsView: View {
|
|
191
|
+
@AppStorage("playbackPrefs") private var prefs = PlaybackPrefs()
|
|
192
|
+
|
|
193
|
+
var body: some View {
|
|
194
|
+
Toggle("Skip silence", isOn: $prefs.skipSilence)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Two details make this bridge safe:
|
|
200
|
+
|
|
201
|
+
- `encode(to:)` and `init(from:)` are written out. When a type is both
|
|
202
|
+
`RawRepresentable` and Codable, the standard library's `RawRepresentable`
|
|
203
|
+
defaults would otherwise encode `rawValue`, which encodes `self`, which
|
|
204
|
+
recurses until the stack overflows.
|
|
205
|
+
- Decoding uses `decodeIfPresent` with defaults, so the `"{}"` fallback and
|
|
206
|
+
values stored by an older version with fewer keys still decode. Synthesized
|
|
207
|
+
decoding would ignore the property defaults and fail on `{}`.
|
|
@@ -82,22 +82,7 @@ SwiftPM `swiftSettings` array, or the Xcode build setting. Use it for app
|
|
|
82
82
|
targets, scripts and other executables where most code serves the UI. Leave
|
|
83
83
|
library targets without it so they stay usable from any isolation.
|
|
84
84
|
|
|
85
|
-
|
|
86
|
-
// Target built with Default Actor Isolation = MainActor: no annotations needed.
|
|
87
|
-
final class PlaybackQueue {
|
|
88
|
-
static let current = PlaybackQueue() // main-actor protected
|
|
89
|
-
var upcoming: [Episode] = [] // main-actor protected
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
final class EpisodeLibrary {
|
|
93
|
-
let renderer = WaveformRenderer()
|
|
94
|
-
var pinned: [Episode.ID] = []
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
extension EpisodeLibrary: Archivable { // conformance is main-actor isolated too
|
|
98
|
-
func archive() -> Data { Data() }
|
|
99
|
-
}
|
|
100
|
-
```
|
|
85
|
+
Code: [Approachable concurrency in Swift 6.2](references/code-examples.md#approachable-concurrency-in-swift-62).
|
|
101
86
|
|
|
102
87
|
**Async functions stay where they are called (SE-0461).** With
|
|
103
88
|
`NonisolatedNonsendingByDefault` on, a `nonisolated` async function runs on the
|
|
@@ -105,22 +90,7 @@ caller's actor instead of jumping to the global concurrent executor. That
|
|
|
105
90
|
behavior is spelled `nonisolated(nonsending)`. Without the feature, Swift 6.0
|
|
106
91
|
and 6.1 semantics still apply and the function leaves the caller's actor.
|
|
107
92
|
|
|
108
|
-
|
|
109
|
-
final class WaveformRenderer {
|
|
110
|
-
func render(_ audio: Data) async -> Waveform { Waveform(samples: []) }
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
@MainActor
|
|
114
|
-
final class EpisodeDetailModel {
|
|
115
|
-
let renderer = WaveformRenderer()
|
|
116
|
-
var waveform: Waveform?
|
|
117
|
-
|
|
118
|
-
func open(_ url: URL) async throws {
|
|
119
|
-
let (audio, _) = try await URLSession.shared.data(from: url)
|
|
120
|
-
waveform = await renderer.render(audio) // stays on the main actor: no race reported
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
```
|
|
93
|
+
Code: [Approachable concurrency in Swift 6.2](references/code-examples.md#approachable-concurrency-in-swift-62).
|
|
124
94
|
|
|
125
95
|
**`@concurrent` asks for the thread pool on purpose.** A `@concurrent` function
|
|
126
96
|
always runs on the concurrent pool, which frees the calling actor. To move a
|
|
@@ -131,32 +101,11 @@ piece of work off the caller:
|
|
|
131
101
|
3. Make the function `async` if it is not already.
|
|
132
102
|
4. Put `await` at each call site.
|
|
133
103
|
|
|
134
|
-
|
|
135
|
-
nonisolated struct TranscriptBuilder {
|
|
136
|
-
@concurrent
|
|
137
|
-
func build(from audio: Data) async -> Transcript? { Transcript(lines: []) }
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// On the main actor:
|
|
141
|
-
transcripts[episode.id] = await builder.build(from: audio)
|
|
142
|
-
```
|
|
104
|
+
Code: [Approachable concurrency in Swift 6.2](references/code-examples.md#approachable-concurrency-in-swift-62).
|
|
143
105
|
|
|
144
|
-
**Other 6.2 additions
|
|
145
|
-
[
|
|
146
|
-
|
|
147
|
-
- `Task.immediate` (SE-0472) runs synchronously on the current actor until its
|
|
148
|
-
first suspension instead of being enqueued; `Task.immediateDetached` adds
|
|
149
|
-
detached semantics. Use it where the enqueue delay is visible to the user,
|
|
150
|
-
for example `Task.immediate { await search.apply(query) }`.
|
|
151
|
-
- `Observations { }` (SE-0475) turns reads of `@Observable` properties into an
|
|
152
|
-
`AsyncSequence` with transactional updates:
|
|
153
|
-
`for await elapsed in Observations({ player.elapsed }) { scrubber.move(to: elapsed) }`.
|
|
154
|
-
- Isolated conformances let a main-actor type conform to a protocol whose
|
|
155
|
-
requirements it can only meet on the main actor. The compiler allows the
|
|
156
|
-
conformance only where that isolation holds.
|
|
157
|
-
- SE-0473 adds `.epoch` to `ContinuousClock` and `SuspendingClock`, but the
|
|
158
|
-
iOS 26.4 SDK does not ship it yet ("value of type 'ContinuousClock' has no
|
|
159
|
-
member 'epoch'"); check your SDK before relying on it.
|
|
106
|
+
**Other 6.2 additions** (`Task.immediate`, `Observations { }`, isolated conformances, clock `.epoch`):
|
|
107
|
+
[swift-6-2-additions.md](references/swift-6-2-additions.md), in depth in
|
|
108
|
+
[concurrency-patterns.md](references/concurrency-patterns.md).
|
|
160
109
|
|
|
161
110
|
## Isolation rules
|
|
162
111
|
|
|
@@ -199,20 +148,7 @@ transcripts[episode.id] = await builder.build(from: audio)
|
|
|
199
148
|
| `Task.immediate { }` | Same as `Task`, starts at once | Latency-sensitive starts (OS 26 runtime) |
|
|
200
149
|
| `Task.detached { }` | Nothing | Deliberately breaking inheritance; see the rule below |
|
|
201
150
|
|
|
202
|
-
|
|
203
|
-
async let profile = api.profile(for: userID)
|
|
204
|
-
async let badges = api.badges(for: userID)
|
|
205
|
-
let (loadedProfile, loadedBadges) = try await (profile, badges)
|
|
206
|
-
|
|
207
|
-
let episodes = try await withThrowingTaskGroup(of: Episode.self) { group in
|
|
208
|
-
for id in episodeIDs {
|
|
209
|
-
group.addTask { try await api.episode(id) }
|
|
210
|
-
}
|
|
211
|
-
var collected: [Episode] = []
|
|
212
|
-
for try await episode in group { collected.append(episode) }
|
|
213
|
-
return collected
|
|
214
|
-
}
|
|
215
|
-
```
|
|
151
|
+
Code: [Tasks and structured concurrency](references/code-examples.md#tasks-and-structured-concurrency).
|
|
216
152
|
|
|
217
153
|
SE-0493 lets a `defer` body `await`, but the Swift 6.3.1 compiler in Xcode
|
|
218
154
|
26.4 still rejects it with "'async' call cannot occur in a defer body". Until
|
|
@@ -247,25 +183,7 @@ Cancellation is a request the task must honor itself.
|
|
|
247
183
|
An actor runs one piece of code at a time, but every `await` inside it lets
|
|
248
184
|
other calls in. State read before an `await` may be stale after it.
|
|
249
185
|
|
|
250
|
-
|
|
251
|
-
actor TicketCounter {
|
|
252
|
-
private var sold = 0
|
|
253
|
-
private let audit: AuditLog
|
|
254
|
-
|
|
255
|
-
init(audit: AuditLog) { self.audit = audit }
|
|
256
|
-
|
|
257
|
-
func sellLosingUpdates() async {
|
|
258
|
-
let current = sold
|
|
259
|
-
await audit.record(current)
|
|
260
|
-
sold = current + 1 // another call may have run during the await
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
func sell() async {
|
|
264
|
-
sold += 1 // read and write with no suspension between them
|
|
265
|
-
await audit.record(sold)
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
```
|
|
186
|
+
Code: [Actor reentrancy](references/code-examples.md#actor-reentrancy).
|
|
269
187
|
|
|
270
188
|
## Streams and continuations
|
|
271
189
|
|
|
@@ -273,15 +191,7 @@ Bridge APIs that call back many times with `AsyncStream`, and single-shot
|
|
|
273
191
|
callbacks with `withCheckedContinuation` or `withCheckedThrowingContinuation`.
|
|
274
192
|
A continuation is resumed exactly once on every path.
|
|
275
193
|
|
|
276
|
-
|
|
277
|
-
func readings(from scale: BluetoothScale) -> AsyncStream<Measurement<UnitMass>> {
|
|
278
|
-
AsyncStream { continuation in
|
|
279
|
-
let subscription = scale.observe { continuation.yield($0) }
|
|
280
|
-
continuation.onTermination = { _ in subscription.cancel() }
|
|
281
|
-
scale.startStreaming()
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
```
|
|
194
|
+
Code: [Streams and continuations](references/code-examples.md#streams-and-continuations).
|
|
285
195
|
|
|
286
196
|
Delegate bridges, cancellation-aware continuations and the full GCD mapping are
|
|
287
197
|
in [bridging-interop.md](references/bridging-interop.md).
|
|
@@ -381,3 +291,6 @@ second choices, each with a named reason.
|
|
|
381
291
|
adoption order, Thread Sanitizer.
|
|
382
292
|
- [async-algorithms.md](references/async-algorithms.md): the
|
|
383
293
|
swift-async-algorithms package.
|
|
294
|
+
- [swift-6-2-additions.md](references/swift-6-2-additions.md): `Task.immediate`,
|
|
295
|
+
`Observations { }`, isolated conformances and the clock `.epoch` status.
|
|
296
|
+
- [Code examples](references/code-examples.md): open for the full code behind Approachable concurrency in Swift 6.2, Tasks and structured concurrency, Actor reentrancy, Streams and continuations.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Swift Concurrency 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
|
+
## Approachable concurrency in Swift 6.2
|
|
6
|
+
|
|
7
|
+
```swift
|
|
8
|
+
// Target built with Default Actor Isolation = MainActor: no annotations needed.
|
|
9
|
+
final class PlaybackQueue {
|
|
10
|
+
static let current = PlaybackQueue() // main-actor protected
|
|
11
|
+
var upcoming: [Episode] = [] // main-actor protected
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
final class EpisodeLibrary {
|
|
15
|
+
let renderer = WaveformRenderer()
|
|
16
|
+
var pinned: [Episode.ID] = []
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
extension EpisodeLibrary: Archivable { // conformance is main-actor isolated too
|
|
20
|
+
func archive() -> Data { Data() }
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```swift
|
|
25
|
+
final class WaveformRenderer {
|
|
26
|
+
func render(_ audio: Data) async -> Waveform { Waveform(samples: []) }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@MainActor
|
|
30
|
+
final class EpisodeDetailModel {
|
|
31
|
+
let renderer = WaveformRenderer()
|
|
32
|
+
var waveform: Waveform?
|
|
33
|
+
|
|
34
|
+
func open(_ url: URL) async throws {
|
|
35
|
+
let (audio, _) = try await URLSession.shared.data(from: url)
|
|
36
|
+
waveform = await renderer.render(audio) // stays on the main actor: no race reported
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```swift
|
|
42
|
+
nonisolated struct TranscriptBuilder {
|
|
43
|
+
@concurrent
|
|
44
|
+
func build(from audio: Data) async -> Transcript? { Transcript(lines: []) }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// On the main actor:
|
|
48
|
+
transcripts[episode.id] = await builder.build(from: audio)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Tasks and structured concurrency
|
|
52
|
+
|
|
53
|
+
```swift
|
|
54
|
+
async let profile = api.profile(for: userID)
|
|
55
|
+
async let badges = api.badges(for: userID)
|
|
56
|
+
let (loadedProfile, loadedBadges) = try await (profile, badges)
|
|
57
|
+
|
|
58
|
+
let episodes = try await withThrowingTaskGroup(of: Episode.self) { group in
|
|
59
|
+
for id in episodeIDs {
|
|
60
|
+
group.addTask { try await api.episode(id) }
|
|
61
|
+
}
|
|
62
|
+
var collected: [Episode] = []
|
|
63
|
+
for try await episode in group { collected.append(episode) }
|
|
64
|
+
return collected
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Actor reentrancy
|
|
69
|
+
|
|
70
|
+
```swift
|
|
71
|
+
actor TicketCounter {
|
|
72
|
+
private var sold = 0
|
|
73
|
+
private let audit: AuditLog
|
|
74
|
+
|
|
75
|
+
init(audit: AuditLog) { self.audit = audit }
|
|
76
|
+
|
|
77
|
+
func sellLosingUpdates() async {
|
|
78
|
+
let current = sold
|
|
79
|
+
await audit.record(current)
|
|
80
|
+
sold = current + 1 // another call may have run during the await
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
func sell() async {
|
|
84
|
+
sold += 1 // read and write with no suspension between them
|
|
85
|
+
await audit.record(sold)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Streams and continuations
|
|
91
|
+
|
|
92
|
+
```swift
|
|
93
|
+
func readings(from scale: BluetoothScale) -> AsyncStream<Measurement<UnitMass>> {
|
|
94
|
+
AsyncStream { continuation in
|
|
95
|
+
let subscription = scale.observe { continuation.yield($0) }
|
|
96
|
+
continuation.onTermination = { _ in subscription.cancel() }
|
|
97
|
+
scale.startStreaming()
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|