@devo-bmad-custom/agent-orchestration 1.0.4 → 1.0.6
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/package.json +1 -1
- package/src/.agents/skills/tmux-commands/SKILL.md +353 -0
- package/src/bmm/data/project-context-template.md +26 -26
- package/src/bmm/teams/default-party.csv +20 -20
- package/src/bmm/workflows/2-plan-workflows/create-prd/data/domain-complexity.csv +14 -14
- package/src/bmm/workflows/2-plan-workflows/create-prd/data/prd-purpose.md +197 -197
- package/src/bmm/workflows/2-plan-workflows/create-prd/data/project-types.csv +10 -10
- package/src/bmm/workflows/2-plan-workflows/create-prd/templates/prd-template.md +10 -10
- package/src/bmm/workflows/3-solutioning/create-architecture/data/domain-complexity.csv +12 -12
- package/src/bmm/workflows/4-implementation/code-review/instructions.xml +226 -226
- package/src/bmm/workflows/4-implementation/correct-course/checklist.md +288 -288
- package/src/bmm/workflows/4-implementation/correct-course/instructions.md +207 -207
- package/src/bmm/workflows/4-implementation/retrospective/instructions.md +1444 -1444
- package/src/bmm/workflows/4-implementation/sprint-planning/sprint-status-template.yaml +55 -55
- package/src/bmm/workflows/4-implementation/sprint-status/instructions.md +230 -230
- package/src/bmm/workflows/bmad-quick-flow/quick-spec/tech-spec-template.md +74 -74
- package/src/bmm/workflows/document-project/instructions.md +130 -130
- package/src/bmm/workflows/document-project/templates/project-scan-report-schema.json +160 -160
- package/src/bmm/workflows/document-project/workflows/deep-dive-instructions.md +298 -298
- package/src/bmm/workflows/document-project/workflows/deep-dive.yaml +31 -31
- package/src/bmm/workflows/document-project/workflows/full-scan-instructions.md +1106 -1106
- package/src/bmm/workflows/document-project/workflows/full-scan.yaml +31 -31
- package/src/bmm/workflows/qa-generate-e2e-tests/checklist.md +33 -33
- package/src/bmm/workflows/qa-generate-e2e-tests/instructions.md +110 -110
- package/src/core/agents/bmad-master.md +56 -56
- package/src/core/workflows/party-mode/steps/step-02-discussion-orchestration.md +187 -187
- package/src/core/workflows/party-mode/steps/step-03-graceful-exit.md +168 -168
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/vision-framework/SKILL.md +0 -475
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/vision-framework/references/vision-requests.md +0 -736
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/vision-framework/references/visionkit-scanner.md +0 -738
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/weatherkit/SKILL.md +0 -410
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/weatherkit/references/weatherkit-patterns.md +0 -567
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/widgetkit/SKILL.md +0 -497
- package/src/.agents/skills/ui-ux-pro-custom/data/swift-ios-skills/widgetkit/references/widgetkit-advanced.md +0 -871
- package/src/.agents/skills/ui-ux-pro-custom/data/typography.csv +0 -58
- package/src/.agents/skills/ui-ux-pro-custom/data/ui-reasoning.csv +0 -101
- package/src/.agents/skills/ui-ux-pro-custom/data/ux-guidelines.csv +0 -100
- package/src/.agents/skills/ui-ux-pro-custom/data/web-interface.csv +0 -31
- package/src/.agents/skills/ui-ux-pro-custom/scripts/core.py +0 -253
- package/src/.agents/skills/ui-ux-pro-custom/scripts/design_system.py +0 -1067
- package/src/.agents/skills/ui-ux-pro-custom/scripts/search.py +0 -114
- package/src/.agents/skills/ux-audit/SKILL.md +0 -151
- package/src/.agents/skills/websocket-engineer/SKILL.md +0 -168
- package/src/.agents/skills/websocket-engineer/references/alternatives.md +0 -391
- package/src/.agents/skills/websocket-engineer/references/patterns.md +0 -400
- package/src/.agents/skills/websocket-engineer/references/protocol.md +0 -195
- package/src/.agents/skills/websocket-engineer/references/scaling.md +0 -333
- package/src/.agents/skills/websocket-engineer/references/security.md +0 -474
- package/src/.agents/skills/writing-skills/SKILL.md +0 -655
- package/src/.agents/skills/writing-skills/anthropic-best-practices.md +0 -1150
- package/src/.agents/skills/writing-skills/examples/CLAUDE_MD_TESTING.md +0 -189
- package/src/.agents/skills/writing-skills/graphviz-conventions.dot +0 -172
- package/src/.agents/skills/writing-skills/persuasion-principles.md +0 -187
- package/src/.agents/skills/writing-skills/render-graphs.js +0 -168
- package/src/.agents/skills/writing-skills/testing-skills-with-subagents.md +0 -384
- package/src/.claude/commands/bmad-track-compact.md +0 -19
- package/src/.claude/commands/bmad-track-extended.md +0 -19
- package/src/.claude/commands/bmad-track-large.md +0 -19
- package/src/.claude/commands/bmad-track-medium.md +0 -19
- package/src/.claude/commands/bmad-track-nano.md +0 -19
- package/src/.claude/commands/bmad-track-rv.md +0 -18
- package/src/.claude/commands/bmad-track-small.md +0 -19
- package/src/.claude/commands/master-orchestrator.md +0 -15
- package/src/_memory/master-orchestrator-sidecar/docs-index.md +0 -3
- package/src/_memory/master-orchestrator-sidecar/instructions.md +0 -2616
- package/src/_memory/master-orchestrator-sidecar/memories.md +0 -8
- package/src/_memory/master-orchestrator-sidecar/session-state.md +0 -15
- package/src/_memory/master-orchestrator-sidecar/triage-history.md +0 -3
- package/src/_memory/master-orchestrator-sidecar/workflows-overview.html +0 -1230
- package/src/core/agents/master-orchestrator.md +0 -54
- package/src/docs/dev/tmux/actions_popup.py +0 -291
- package/src/docs/dev/tmux/actions_popup.sh +0 -110
- package/src/docs/dev/tmux/claude_usage.sh +0 -15
- package/src/docs/dev/tmux/colors.conf +0 -26
- package/src/docs/dev/tmux/cpu_usage.sh +0 -7
- package/src/docs/dev/tmux/dispatch.sh +0 -10
- package/src/docs/dev/tmux/float_init.sh +0 -13
- package/src/docs/dev/tmux/float_term.sh +0 -23
- package/src/docs/dev/tmux/open_clip.sh +0 -14
- package/src/docs/dev/tmux/paste_clipboard.sh +0 -13
- package/src/docs/dev/tmux/paste_image_wrapper.sh +0 -94
- package/src/docs/dev/tmux/ram_usage.sh +0 -3
- package/src/docs/dev/tmux/title_sync.sh +0 -54
- package/src/docs/dev/tmux/tmux-setup.md +0 -867
- package/src/docs/dev/tmux/tmux.conf +0 -127
- package/src/docs/dev/tmux/xclip +0 -18
|
@@ -1,738 +0,0 @@
|
|
|
1
|
-
# VisionKit Scanner Patterns
|
|
2
|
-
|
|
3
|
-
Complete implementation patterns for DataScannerViewController and
|
|
4
|
-
VNDocumentCameraViewController covering availability checking, configuration,
|
|
5
|
-
SwiftUI integration, delegate handling, custom overlays, and camera permissions.
|
|
6
|
-
All patterns target iOS 26+ with Swift 6.2 unless noted.
|
|
7
|
-
|
|
8
|
-
## Contents
|
|
9
|
-
- Camera Permission Setup
|
|
10
|
-
- DataScannerViewController
|
|
11
|
-
- Delegate Methods
|
|
12
|
-
- SwiftUI Integration
|
|
13
|
-
- Custom Overlay UI
|
|
14
|
-
- VNDocumentCameraViewController
|
|
15
|
-
|
|
16
|
-
## Camera Permission Setup
|
|
17
|
-
|
|
18
|
-
Add the camera usage description to Info.plist before using any scanner:
|
|
19
|
-
|
|
20
|
-
```xml
|
|
21
|
-
<key>NSCameraUsageDescription</key>
|
|
22
|
-
<string>Camera access is needed to scan text and barcodes.</string>
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
Request permission before presenting the scanner:
|
|
26
|
-
|
|
27
|
-
```swift
|
|
28
|
-
import AVFoundation
|
|
29
|
-
|
|
30
|
-
func requestCameraAccess() async -> Bool {
|
|
31
|
-
let status = AVCaptureDevice.authorizationStatus(for: .video)
|
|
32
|
-
switch status {
|
|
33
|
-
case .authorized:
|
|
34
|
-
return true
|
|
35
|
-
case .notDetermined:
|
|
36
|
-
return await AVCaptureDevice.requestAccess(for: .video)
|
|
37
|
-
case .denied, .restricted:
|
|
38
|
-
return false
|
|
39
|
-
@unknown default:
|
|
40
|
-
return false
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
## DataScannerViewController
|
|
46
|
-
|
|
47
|
-
`DataScannerViewController` provides a full-screen live camera scanner for text
|
|
48
|
-
and barcodes with built-in highlighting and interaction. Available on devices
|
|
49
|
-
with an A12 chip or later (iOS 16+).
|
|
50
|
-
|
|
51
|
-
### Availability Checking
|
|
52
|
-
|
|
53
|
-
Always check both hardware support and runtime availability before presenting.
|
|
54
|
-
|
|
55
|
-
```swift
|
|
56
|
-
import VisionKit
|
|
57
|
-
|
|
58
|
-
func canUseDataScanner() -> Bool {
|
|
59
|
-
// Hardware check: requires A12 Bionic or later
|
|
60
|
-
guard DataScannerViewController.isSupported else {
|
|
61
|
-
return false
|
|
62
|
-
}
|
|
63
|
-
// Runtime check: camera authorized and not restricted
|
|
64
|
-
guard DataScannerViewController.isAvailable else {
|
|
65
|
-
return false
|
|
66
|
-
}
|
|
67
|
-
return true
|
|
68
|
-
}
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
`isSupported` checks hardware capability (A12+). `isAvailable` checks that the
|
|
72
|
-
camera is authorized and not restricted by device management. Both must be true.
|
|
73
|
-
|
|
74
|
-
### Configuration and Initialization
|
|
75
|
-
|
|
76
|
-
```swift
|
|
77
|
-
import VisionKit
|
|
78
|
-
|
|
79
|
-
func createTextScanner() -> DataScannerViewController {
|
|
80
|
-
DataScannerViewController(
|
|
81
|
-
recognizedDataTypes: [
|
|
82
|
-
.text(languages: ["en"]),
|
|
83
|
-
],
|
|
84
|
-
qualityLevel: .balanced,
|
|
85
|
-
recognizesMultipleItems: true,
|
|
86
|
-
isHighFrameRateTrackingEnabled: true,
|
|
87
|
-
isPinchToZoomEnabled: true,
|
|
88
|
-
isGuidanceEnabled: true,
|
|
89
|
-
isHighlightingEnabled: true
|
|
90
|
-
)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
func createBarcodeScanner() -> DataScannerViewController {
|
|
94
|
-
DataScannerViewController(
|
|
95
|
-
recognizedDataTypes: [
|
|
96
|
-
.barcode(symbologies: [.qr, .ean13, .code128]),
|
|
97
|
-
],
|
|
98
|
-
qualityLevel: .fast,
|
|
99
|
-
recognizesMultipleItems: false,
|
|
100
|
-
isHighFrameRateTrackingEnabled: false,
|
|
101
|
-
isPinchToZoomEnabled: false,
|
|
102
|
-
isGuidanceEnabled: true,
|
|
103
|
-
isHighlightingEnabled: true
|
|
104
|
-
)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
func createMixedScanner() -> DataScannerViewController {
|
|
108
|
-
DataScannerViewController(
|
|
109
|
-
recognizedDataTypes: [
|
|
110
|
-
.text(languages: ["en"]),
|
|
111
|
-
.barcode(symbologies: [.qr, .ean13]),
|
|
112
|
-
],
|
|
113
|
-
qualityLevel: .balanced,
|
|
114
|
-
recognizesMultipleItems: true,
|
|
115
|
-
isHighFrameRateTrackingEnabled: true,
|
|
116
|
-
isPinchToZoomEnabled: true,
|
|
117
|
-
isGuidanceEnabled: true,
|
|
118
|
-
isHighlightingEnabled: true
|
|
119
|
-
)
|
|
120
|
-
}
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
### Recognized Data Types
|
|
124
|
-
|
|
125
|
-
```swift
|
|
126
|
-
// Text with language hints
|
|
127
|
-
let textType: DataScannerViewController.RecognizedDataType =
|
|
128
|
-
.text(languages: ["en", "fr", "de"])
|
|
129
|
-
|
|
130
|
-
// Text filtered by content type
|
|
131
|
-
let emailType: DataScannerViewController.RecognizedDataType =
|
|
132
|
-
.text(textContentType: .emailAddress)
|
|
133
|
-
let urlType: DataScannerViewController.RecognizedDataType =
|
|
134
|
-
.text(textContentType: .URL)
|
|
135
|
-
let phoneType: DataScannerViewController.RecognizedDataType =
|
|
136
|
-
.text(textContentType: .telephoneNumber)
|
|
137
|
-
let addressType: DataScannerViewController.RecognizedDataType =
|
|
138
|
-
.text(textContentType: .fullAddress)
|
|
139
|
-
let flightType: DataScannerViewController.RecognizedDataType =
|
|
140
|
-
.text(textContentType: .flightNumber)
|
|
141
|
-
let trackingType: DataScannerViewController.RecognizedDataType =
|
|
142
|
-
.text(textContentType: .shipmentTrackingNumber)
|
|
143
|
-
|
|
144
|
-
// Barcode with specific symbologies
|
|
145
|
-
let qrOnly: DataScannerViewController.RecognizedDataType =
|
|
146
|
-
.barcode(symbologies: [.qr])
|
|
147
|
-
let retailBarcodes: DataScannerViewController.RecognizedDataType =
|
|
148
|
-
.barcode(symbologies: [.ean8, .ean13, .upce, .code128])
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
### Quality Levels
|
|
152
|
-
|
|
153
|
-
| Level | Use Case | Notes |
|
|
154
|
-
|---|---|---|
|
|
155
|
-
| `.fast` | Barcode scanning, quick text grab | Lowest latency |
|
|
156
|
-
| `.balanced` | General purpose text + barcode | Default choice |
|
|
157
|
-
| `.accurate` | Detailed OCR, small text | Higher latency |
|
|
158
|
-
|
|
159
|
-
### Starting and Stopping
|
|
160
|
-
|
|
161
|
-
```swift
|
|
162
|
-
func presentScanner(_ scanner: DataScannerViewController,
|
|
163
|
-
from presenter: UIViewController) {
|
|
164
|
-
scanner.delegate = presenter as? DataScannerViewControllerDelegate
|
|
165
|
-
presenter.present(scanner, animated: true) {
|
|
166
|
-
try? scanner.startScanning()
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
func dismissScanner(_ scanner: DataScannerViewController) {
|
|
171
|
-
scanner.stopScanning()
|
|
172
|
-
scanner.dismiss(animated: true)
|
|
173
|
-
}
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
## Delegate Methods
|
|
177
|
-
|
|
178
|
-
Implement `DataScannerViewControllerDelegate` to handle recognized items and
|
|
179
|
-
scanner lifecycle events.
|
|
180
|
-
|
|
181
|
-
```swift
|
|
182
|
-
import VisionKit
|
|
183
|
-
|
|
184
|
-
final class ScannerCoordinator: NSObject, DataScannerViewControllerDelegate {
|
|
185
|
-
|
|
186
|
-
var onTextRecognized: ((String) -> Void)?
|
|
187
|
-
var onBarcodeRecognized: ((String, VNBarcodeSymbology) -> Void)?
|
|
188
|
-
|
|
189
|
-
// Called when the user taps on a recognized item
|
|
190
|
-
func dataScanner(
|
|
191
|
-
_ scanner: DataScannerViewController,
|
|
192
|
-
didTapOn item: RecognizedItem
|
|
193
|
-
) {
|
|
194
|
-
switch item {
|
|
195
|
-
case .text(let text):
|
|
196
|
-
onTextRecognized?(text.transcript)
|
|
197
|
-
case .barcode(let barcode):
|
|
198
|
-
if let payload = barcode.payloadStringValue {
|
|
199
|
-
onBarcodeRecognized?(payload, barcode.observation.symbology)
|
|
200
|
-
}
|
|
201
|
-
@unknown default:
|
|
202
|
-
break
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Called when new items appear in the camera view
|
|
207
|
-
func dataScanner(
|
|
208
|
-
_ scanner: DataScannerViewController,
|
|
209
|
-
didAdd addedItems: [RecognizedItem],
|
|
210
|
-
allItems: [RecognizedItem]
|
|
211
|
-
) {
|
|
212
|
-
for item in addedItems {
|
|
213
|
-
switch item {
|
|
214
|
-
case .text(let text):
|
|
215
|
-
print("New text: \(text.transcript)")
|
|
216
|
-
case .barcode(let barcode):
|
|
217
|
-
print("New barcode: \(barcode.payloadStringValue ?? "nil")")
|
|
218
|
-
@unknown default:
|
|
219
|
-
break
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Called when items are updated (position or content changes)
|
|
225
|
-
func dataScanner(
|
|
226
|
-
_ scanner: DataScannerViewController,
|
|
227
|
-
didUpdate updatedItems: [RecognizedItem],
|
|
228
|
-
allItems: [RecognizedItem]
|
|
229
|
-
) {
|
|
230
|
-
// Handle position or content updates
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Called when items leave the camera view
|
|
234
|
-
func dataScanner(
|
|
235
|
-
_ scanner: DataScannerViewController,
|
|
236
|
-
didRemove removedItems: [RecognizedItem],
|
|
237
|
-
allItems: [RecognizedItem]
|
|
238
|
-
) {
|
|
239
|
-
// Clean up UI for removed items
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Called when the scanner becomes unavailable (e.g., camera revoked)
|
|
243
|
-
func dataScannerDidChangeUnavailabilityReasons(
|
|
244
|
-
_ scanner: DataScannerViewController
|
|
245
|
-
) {
|
|
246
|
-
// Handle unavailability -- dismiss or show fallback
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
```
|
|
250
|
-
|
|
251
|
-
### Async Sequence for Recognized Items
|
|
252
|
-
|
|
253
|
-
Use `recognizedItems` for a reactive stream of all currently visible items:
|
|
254
|
-
|
|
255
|
-
```swift
|
|
256
|
-
func observeRecognizedItems(_ scanner: DataScannerViewController) async {
|
|
257
|
-
for await items in scanner.recognizedItems {
|
|
258
|
-
let texts = items.compactMap { item -> String? in
|
|
259
|
-
guard case .text(let text) = item else { return nil }
|
|
260
|
-
return text.transcript
|
|
261
|
-
}
|
|
262
|
-
let barcodes = items.compactMap { item -> String? in
|
|
263
|
-
guard case .barcode(let barcode) = item else { return nil }
|
|
264
|
-
return barcode.payloadStringValue
|
|
265
|
-
}
|
|
266
|
-
await MainActor.run {
|
|
267
|
-
// Update UI with current texts and barcodes
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
### Capturing a Photo
|
|
274
|
-
|
|
275
|
-
Capture a still image from the scanner for further processing:
|
|
276
|
-
|
|
277
|
-
```swift
|
|
278
|
-
func captureAndProcess(_ scanner: DataScannerViewController) async throws {
|
|
279
|
-
let photo = try await scanner.capturePhoto()
|
|
280
|
-
// photo is a UIImage -- process with Vision or save
|
|
281
|
-
}
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
## SwiftUI Integration
|
|
285
|
-
|
|
286
|
-
Wrap `DataScannerViewController` in `UIViewControllerRepresentable` for use
|
|
287
|
-
in SwiftUI views.
|
|
288
|
-
|
|
289
|
-
### Full DataScanner Representable
|
|
290
|
-
|
|
291
|
-
```swift
|
|
292
|
-
import SwiftUI
|
|
293
|
-
import VisionKit
|
|
294
|
-
|
|
295
|
-
struct DataScannerRepresentable: UIViewControllerRepresentable {
|
|
296
|
-
let recognizedDataTypes: Set<DataScannerViewController.RecognizedDataType>
|
|
297
|
-
let qualityLevel: DataScannerViewController.QualityLevel
|
|
298
|
-
let recognizesMultipleItems: Bool
|
|
299
|
-
@Binding var recognizedText: [String]
|
|
300
|
-
@Binding var recognizedBarcodes: [String]
|
|
301
|
-
|
|
302
|
-
func makeUIViewController(context: Context) -> DataScannerViewController {
|
|
303
|
-
let scanner = DataScannerViewController(
|
|
304
|
-
recognizedDataTypes: recognizedDataTypes,
|
|
305
|
-
qualityLevel: qualityLevel,
|
|
306
|
-
recognizesMultipleItems: recognizesMultipleItems,
|
|
307
|
-
isHighFrameRateTrackingEnabled: true,
|
|
308
|
-
isPinchToZoomEnabled: true,
|
|
309
|
-
isGuidanceEnabled: true,
|
|
310
|
-
isHighlightingEnabled: true
|
|
311
|
-
)
|
|
312
|
-
scanner.delegate = context.coordinator
|
|
313
|
-
return scanner
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
func updateUIViewController(
|
|
317
|
-
_ controller: DataScannerViewController,
|
|
318
|
-
context: Context
|
|
319
|
-
) {
|
|
320
|
-
// No dynamic updates needed
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
func makeCoordinator() -> Coordinator {
|
|
324
|
-
Coordinator(parent: self)
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
static func dismantleUIViewController(
|
|
328
|
-
_ controller: DataScannerViewController,
|
|
329
|
-
coordinator: Coordinator
|
|
330
|
-
) {
|
|
331
|
-
controller.stopScanning()
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
@MainActor
|
|
335
|
-
final class Coordinator: NSObject, DataScannerViewControllerDelegate {
|
|
336
|
-
let parent: DataScannerRepresentable
|
|
337
|
-
|
|
338
|
-
init(parent: DataScannerRepresentable) {
|
|
339
|
-
self.parent = parent
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
func dataScanner(
|
|
343
|
-
_ scanner: DataScannerViewController,
|
|
344
|
-
didTapOn item: RecognizedItem
|
|
345
|
-
) {
|
|
346
|
-
switch item {
|
|
347
|
-
case .text(let text):
|
|
348
|
-
parent.recognizedText.append(text.transcript)
|
|
349
|
-
case .barcode(let barcode):
|
|
350
|
-
if let payload = barcode.payloadStringValue {
|
|
351
|
-
parent.recognizedBarcodes.append(payload)
|
|
352
|
-
}
|
|
353
|
-
@unknown default:
|
|
354
|
-
break
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
func dataScanner(
|
|
359
|
-
_ scanner: DataScannerViewController,
|
|
360
|
-
didAdd addedItems: [RecognizedItem],
|
|
361
|
-
allItems: [RecognizedItem]
|
|
362
|
-
) {
|
|
363
|
-
// Handle newly recognized items
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
func dataScanner(
|
|
367
|
-
_ scanner: DataScannerViewController,
|
|
368
|
-
didUpdate updatedItems: [RecognizedItem],
|
|
369
|
-
allItems: [RecognizedItem]
|
|
370
|
-
) {
|
|
371
|
-
// Handle item updates
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
func dataScanner(
|
|
375
|
-
_ scanner: DataScannerViewController,
|
|
376
|
-
didRemove removedItems: [RecognizedItem],
|
|
377
|
-
allItems: [RecognizedItem]
|
|
378
|
-
) {
|
|
379
|
-
// Handle removed items
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
```
|
|
384
|
-
|
|
385
|
-
### SwiftUI Scanner View
|
|
386
|
-
|
|
387
|
-
```swift
|
|
388
|
-
import SwiftUI
|
|
389
|
-
import VisionKit
|
|
390
|
-
|
|
391
|
-
struct ScannerView: View {
|
|
392
|
-
@State private var recognizedText: [String] = []
|
|
393
|
-
@State private var recognizedBarcodes: [String] = []
|
|
394
|
-
@State private var isShowingScanner = false
|
|
395
|
-
|
|
396
|
-
var body: some View {
|
|
397
|
-
VStack {
|
|
398
|
-
if DataScannerViewController.isSupported {
|
|
399
|
-
Button("Scan") {
|
|
400
|
-
isShowingScanner = true
|
|
401
|
-
}
|
|
402
|
-
.fullScreenCover(isPresented: $isShowingScanner) {
|
|
403
|
-
NavigationStack {
|
|
404
|
-
DataScannerRepresentable(
|
|
405
|
-
recognizedDataTypes: [
|
|
406
|
-
.text(languages: ["en"]),
|
|
407
|
-
.barcode(symbologies: [.qr]),
|
|
408
|
-
],
|
|
409
|
-
qualityLevel: .balanced,
|
|
410
|
-
recognizesMultipleItems: true,
|
|
411
|
-
recognizedText: $recognizedText,
|
|
412
|
-
recognizedBarcodes: $recognizedBarcodes
|
|
413
|
-
)
|
|
414
|
-
.ignoresSafeArea()
|
|
415
|
-
.toolbar {
|
|
416
|
-
ToolbarItem(placement: .cancellationAction) {
|
|
417
|
-
Button("Done") {
|
|
418
|
-
isShowingScanner = false
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
} else {
|
|
425
|
-
ContentUnavailableView(
|
|
426
|
-
"Scanner Not Available",
|
|
427
|
-
systemImage: "camera.fill",
|
|
428
|
-
description: Text("This device does not support scanning.")
|
|
429
|
-
)
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
List {
|
|
433
|
-
Section("Text") {
|
|
434
|
-
ForEach(recognizedText, id: \.self) { text in
|
|
435
|
-
Text(text)
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
Section("Barcodes") {
|
|
439
|
-
ForEach(recognizedBarcodes, id: \.self) { barcode in
|
|
440
|
-
Text(barcode)
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
```
|
|
448
|
-
|
|
449
|
-
### Starting the Scanner After Presentation
|
|
450
|
-
|
|
451
|
-
The scanner must be started after the view controller is fully presented.
|
|
452
|
-
Use `onAppear` with a coordinator flag or start in the completion handler:
|
|
453
|
-
|
|
454
|
-
```swift
|
|
455
|
-
struct AutoStartScannerRepresentable: UIViewControllerRepresentable {
|
|
456
|
-
func makeUIViewController(context: Context) -> DataScannerViewController {
|
|
457
|
-
let scanner = DataScannerViewController(
|
|
458
|
-
recognizedDataTypes: [.text(languages: ["en"])],
|
|
459
|
-
qualityLevel: .balanced,
|
|
460
|
-
recognizesMultipleItems: false,
|
|
461
|
-
isHighFrameRateTrackingEnabled: true,
|
|
462
|
-
isHighlightingEnabled: true
|
|
463
|
-
)
|
|
464
|
-
scanner.delegate = context.coordinator
|
|
465
|
-
// Start scanning after a brief delay to ensure presentation is complete
|
|
466
|
-
Task { @MainActor in
|
|
467
|
-
try? scanner.startScanning()
|
|
468
|
-
}
|
|
469
|
-
return scanner
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
func updateUIViewController(
|
|
473
|
-
_ controller: DataScannerViewController,
|
|
474
|
-
context: Context
|
|
475
|
-
) {}
|
|
476
|
-
|
|
477
|
-
func makeCoordinator() -> ScannerCoordinator {
|
|
478
|
-
ScannerCoordinator()
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
static func dismantleUIViewController(
|
|
482
|
-
_ controller: DataScannerViewController,
|
|
483
|
-
coordinator: ScannerCoordinator
|
|
484
|
-
) {
|
|
485
|
-
controller.stopScanning()
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
```
|
|
489
|
-
|
|
490
|
-
## Custom Overlay UI
|
|
491
|
-
|
|
492
|
-
Add custom views on top of the scanner for region-of-interest indicators,
|
|
493
|
-
instructions, or result display.
|
|
494
|
-
|
|
495
|
-
### Overlay with Region of Interest
|
|
496
|
-
|
|
497
|
-
```swift
|
|
498
|
-
struct ScannerWithOverlay: View {
|
|
499
|
-
@State private var isShowingScanner = false
|
|
500
|
-
@State private var lastScannedText = ""
|
|
501
|
-
|
|
502
|
-
var body: some View {
|
|
503
|
-
ZStack {
|
|
504
|
-
AutoStartScannerRepresentable()
|
|
505
|
-
.ignoresSafeArea()
|
|
506
|
-
|
|
507
|
-
VStack {
|
|
508
|
-
// Top instruction bar
|
|
509
|
-
Text("Point camera at text or barcode")
|
|
510
|
-
.font(.subheadline)
|
|
511
|
-
.padding(.horizontal, 16)
|
|
512
|
-
.padding(.vertical, 8)
|
|
513
|
-
.background(.ultraThinMaterial, in: Capsule())
|
|
514
|
-
.padding(.top, 60)
|
|
515
|
-
|
|
516
|
-
Spacer()
|
|
517
|
-
|
|
518
|
-
// Scan region indicator
|
|
519
|
-
RoundedRectangle(cornerRadius: 12)
|
|
520
|
-
.strokeBorder(.white.opacity(0.6), lineWidth: 2)
|
|
521
|
-
.frame(width: 280, height: 180)
|
|
522
|
-
|
|
523
|
-
Spacer()
|
|
524
|
-
|
|
525
|
-
// Result display
|
|
526
|
-
if !lastScannedText.isEmpty {
|
|
527
|
-
Text(lastScannedText)
|
|
528
|
-
.font(.body)
|
|
529
|
-
.padding()
|
|
530
|
-
.frame(maxWidth: .infinity)
|
|
531
|
-
.background(.ultraThinMaterial)
|
|
532
|
-
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
533
|
-
.padding()
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
```
|
|
540
|
-
|
|
541
|
-
## VNDocumentCameraViewController
|
|
542
|
-
|
|
543
|
-
`VNDocumentCameraViewController` provides a full-screen document camera with
|
|
544
|
-
auto-capture, perspective correction, and multi-page scanning. Available on
|
|
545
|
-
all devices running iOS 13+.
|
|
546
|
-
|
|
547
|
-
### UIKit Presentation
|
|
548
|
-
|
|
549
|
-
```swift
|
|
550
|
-
import VisionKit
|
|
551
|
-
|
|
552
|
-
final class DocumentScannerPresenter: NSObject,
|
|
553
|
-
VNDocumentCameraViewControllerDelegate
|
|
554
|
-
{
|
|
555
|
-
weak var presenter: UIViewController?
|
|
556
|
-
|
|
557
|
-
func showDocumentScanner() {
|
|
558
|
-
let scanner = VNDocumentCameraViewController()
|
|
559
|
-
scanner.delegate = self
|
|
560
|
-
presenter?.present(scanner, animated: true)
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
func documentCameraViewController(
|
|
564
|
-
_ controller: VNDocumentCameraViewController,
|
|
565
|
-
didFinishWith scan: VNDocumentCameraScan
|
|
566
|
-
) {
|
|
567
|
-
controller.dismiss(animated: true)
|
|
568
|
-
for pageIndex in 0..<scan.pageCount {
|
|
569
|
-
let pageImage = scan.imageOfPage(at: pageIndex)
|
|
570
|
-
// Process each scanned page image
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
func documentCameraViewControllerDidCancel(
|
|
575
|
-
_ controller: VNDocumentCameraViewController
|
|
576
|
-
) {
|
|
577
|
-
controller.dismiss(animated: true)
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
func documentCameraViewController(
|
|
581
|
-
_ controller: VNDocumentCameraViewController,
|
|
582
|
-
didFailWithError error: Error
|
|
583
|
-
) {
|
|
584
|
-
controller.dismiss(animated: true)
|
|
585
|
-
// Handle scanning error
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
```
|
|
589
|
-
|
|
590
|
-
### SwiftUI Document Scanner
|
|
591
|
-
|
|
592
|
-
```swift
|
|
593
|
-
import SwiftUI
|
|
594
|
-
import VisionKit
|
|
595
|
-
|
|
596
|
-
struct DocumentScannerRepresentable: UIViewControllerRepresentable {
|
|
597
|
-
@Binding var scannedImages: [UIImage]
|
|
598
|
-
@Environment(\.dismiss) private var dismiss
|
|
599
|
-
|
|
600
|
-
func makeUIViewController(context: Context) -> VNDocumentCameraViewController {
|
|
601
|
-
let scanner = VNDocumentCameraViewController()
|
|
602
|
-
scanner.delegate = context.coordinator
|
|
603
|
-
return scanner
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
func updateUIViewController(
|
|
607
|
-
_ controller: VNDocumentCameraViewController,
|
|
608
|
-
context: Context
|
|
609
|
-
) {}
|
|
610
|
-
|
|
611
|
-
func makeCoordinator() -> Coordinator {
|
|
612
|
-
Coordinator(parent: self)
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
@MainActor
|
|
616
|
-
final class Coordinator: NSObject, VNDocumentCameraViewControllerDelegate {
|
|
617
|
-
let parent: DocumentScannerRepresentable
|
|
618
|
-
|
|
619
|
-
init(parent: DocumentScannerRepresentable) {
|
|
620
|
-
self.parent = parent
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
func documentCameraViewController(
|
|
624
|
-
_ controller: VNDocumentCameraViewController,
|
|
625
|
-
didFinishWith scan: VNDocumentCameraScan
|
|
626
|
-
) {
|
|
627
|
-
parent.scannedImages = (0..<scan.pageCount).map { scan.imageOfPage(at: $0) }
|
|
628
|
-
parent.dismiss()
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
func documentCameraViewControllerDidCancel(
|
|
632
|
-
_ controller: VNDocumentCameraViewController
|
|
633
|
-
) {
|
|
634
|
-
parent.dismiss()
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
func documentCameraViewController(
|
|
638
|
-
_ controller: VNDocumentCameraViewController,
|
|
639
|
-
didFailWithError error: Error
|
|
640
|
-
) {
|
|
641
|
-
parent.dismiss()
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
```
|
|
646
|
-
|
|
647
|
-
### Document Scanner with OCR Pipeline
|
|
648
|
-
|
|
649
|
-
Combine document scanning with Vision text recognition for a complete OCR flow:
|
|
650
|
-
|
|
651
|
-
```swift
|
|
652
|
-
import SwiftUI
|
|
653
|
-
import VisionKit
|
|
654
|
-
import Vision
|
|
655
|
-
|
|
656
|
-
@MainActor
|
|
657
|
-
@Observable
|
|
658
|
-
final class DocumentOCRModel {
|
|
659
|
-
var scannedPages: [UIImage] = []
|
|
660
|
-
var extractedText: [String] = []
|
|
661
|
-
var isProcessing = false
|
|
662
|
-
|
|
663
|
-
func processScannedPages() async {
|
|
664
|
-
isProcessing = true
|
|
665
|
-
defer { isProcessing = false }
|
|
666
|
-
|
|
667
|
-
extractedText = []
|
|
668
|
-
for page in scannedPages {
|
|
669
|
-
guard let cgImage = page.cgImage else { continue }
|
|
670
|
-
do {
|
|
671
|
-
var request = RecognizeTextRequest()
|
|
672
|
-
request.recognitionLevel = .accurate
|
|
673
|
-
request.recognitionLanguages = [Locale.Language(identifier: "en-US")]
|
|
674
|
-
request.usesLanguageCorrection = true
|
|
675
|
-
|
|
676
|
-
let observations = try await request.perform(on: cgImage)
|
|
677
|
-
let pageText = observations
|
|
678
|
-
.compactMap { $0.topCandidates(1).first?.string }
|
|
679
|
-
.joined(separator: "\n")
|
|
680
|
-
extractedText.append(pageText)
|
|
681
|
-
} catch {
|
|
682
|
-
extractedText.append("[Recognition failed]")
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
struct DocumentOCRView: View {
|
|
689
|
-
@State private var model = DocumentOCRModel()
|
|
690
|
-
@State private var isShowingScanner = false
|
|
691
|
-
|
|
692
|
-
var body: some View {
|
|
693
|
-
NavigationStack {
|
|
694
|
-
List {
|
|
695
|
-
if model.isProcessing {
|
|
696
|
-
ProgressView("Recognizing text...")
|
|
697
|
-
}
|
|
698
|
-
ForEach(Array(model.extractedText.enumerated()), id: \.offset) { index, text in
|
|
699
|
-
Section("Page \(index + 1)") {
|
|
700
|
-
Text(text)
|
|
701
|
-
.font(.body)
|
|
702
|
-
.textSelection(.enabled)
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
.navigationTitle("Document OCR")
|
|
707
|
-
.toolbar {
|
|
708
|
-
Button("Scan") {
|
|
709
|
-
isShowingScanner = true
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
.fullScreenCover(isPresented: $isShowingScanner) {
|
|
713
|
-
DocumentScannerRepresentable(scannedImages: $model.scannedPages)
|
|
714
|
-
}
|
|
715
|
-
.onChange(of: model.scannedPages) {
|
|
716
|
-
Task { await model.processScannedPages() }
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
```
|
|
722
|
-
|
|
723
|
-
## Performance Considerations
|
|
724
|
-
|
|
725
|
-
### DataScannerViewController
|
|
726
|
-
|
|
727
|
-
- Use `.fast` quality for barcode-only scanning
|
|
728
|
-
- Set `recognizesMultipleItems = false` when only one result is needed
|
|
729
|
-
- Disable `isHighFrameRateTrackingEnabled` for barcode scanning to save power
|
|
730
|
-
- Limit `recognizedDataTypes` to only what you need
|
|
731
|
-
- Stop scanning when processing results to avoid wasted CPU cycles
|
|
732
|
-
|
|
733
|
-
### VNDocumentCameraViewController
|
|
734
|
-
|
|
735
|
-
- Pages are returned as `UIImage` at full resolution -- resize before
|
|
736
|
-
processing if memory is a concern
|
|
737
|
-
- Process pages sequentially to avoid memory spikes
|
|
738
|
-
- Use `autoreleasepool` when processing many pages in a loop
|