Add UIViewRepresentable integration to the SparkScan SwiftUI guide#381
Add UIViewRepresentable integration to the SparkScan SwiftUI guide#381ts1mafei wants to merge 4 commits into
Conversation
Documents the pure-SwiftUI alternative to the withSparkScan view modifier: wrapping SparkScanView directly with UIViewRepresentable and a coordinator, including how to apply setting changes to the running scanner (SDC-31151). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
marcozoffoli-scandit
left a comment
There was a problem hiding this comment.
Thanks for adding this — nice addition to the SwiftUI guide, and the threading callouts are a great touch. I left a few small, non-blocking suggestions inline, mostly around making the example as copy-paste-robust as possible for readers.
| // Push the current SwiftUI state into the running scanner. | ||
| context.coordinator.applySymbologiesIfNeeded(symbologies) | ||
| if isActive { | ||
| context.coordinator.sparkScanView?.prepareScanning() |
There was a problem hiding this comment.
updateUIView runs on every SwiftUI state change — in the usage example below, that includes every scanned barcode being appended to scannedBarcodes. As written, prepareScanning() is therefore re-invoked once per scan while the scanner is active.
If a redundant prepareScanning() is a guaranteed no-op in the SDK this is harmless, but it might be safer not to rely on readers knowing that. One option is to have the coordinator remember the last applied value and only call prepareScanning()/stopScanning() on transitions, e.g.:
if isActive != context.coordinator.isScanningActive {
context.coordinator.isScanningActive = isActive
isActive ? context.coordinator.sparkScanView?.prepareScanning()
: context.coordinator.sparkScanView?.stopScanning()
}That would also make this consistent with the diff-before-apply pattern used in applySymbologiesIfNeeded below.
There was a problem hiding this comment.
🤖 Good catch — and your hedge was actually too generous: a redundant prepareScanning() is not a no-op. In the SDK it unconditionally resets the view state to Idle and re-applies camera settings (SDCSparkScanViewManager.mm, prepareScanning), so calling it per render can interrupt active scanning after every recognized barcode. Adopted your transition guard (via coordinator.isScanningActive) in fa03219 — it also keeps the example consistent with the diff-before-apply pattern in applySymbologiesIfNeeded. The change is verified in the compiling companion sample (ListBuildingSampleSwiftUIRepresentable in data-capture-sdk).
| context.coordinator.sparkScanView?.stopScanning() | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Small suggestion: when the scanner view is removed from the hierarchy (navigation pop, conditional view, etc.) while isActive is still true, stopScanning() is never called and cleanup relies on deallocation. Adding a dismantleUIView would model the full lifecycle for readers:
static func dismantleUIView(_ uiView: UIView, coordinator: SparkScanScannerCoordinator) {
coordinator.sparkScanView?.stopScanning()
}There was a problem hiding this comment.
🤖 Agreed — added dismantleUIView calling stopScanning() in fa03219, so the teardown path is modeled too.
| extension SparkScanScannerCoordinator: SparkScanFeedbackDelegate { | ||
| // Invoked on a background queue: the closure must only access | ||
| // thread-safe state. | ||
| func feedback(for barcode: Barcode) -> SparkScanBarcodeFeedback? { |
There was a problem hiding this comment.
Minor thread-safety note: this delegate method reads self.feedbackFor from a background queue, while update(onScan:feedbackFor:) reassigns the same property from the main thread on every render — so there's an unsynchronized read/write on the coordinator's stored closures themselves (slightly at odds with the comment above, which warns about state accessed inside the closure).
For a docs example this may well be an acceptable simplification, but it could be worth either protecting the closure properties with a small lock, or extending the comment to acknowledge the simplification so readers don't copy it into more complex setups unaware.
There was a problem hiding this comment.
🤖 Fair point — the read/write on the stored closures themselves is indeed unsynchronized. Went with your lighter option to keep the get-started example lean: the comment now explicitly acknowledges the simplification and tells readers to add locking around the stored closures if their app replaces them frequently (fa03219).
…e note - Call prepareScanning/stopScanning only on isActive transitions, since updateUIView runs on every SwiftUI state change and prepareScanning resets the scanning state - Add dismantleUIView so scanning stops when the view leaves the hierarchy - Acknowledge the unsynchronized closure storage as a simplification Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
What
Expands the iOS SparkScan Get Started With SwiftUI guide with a new section documenting the pure-SwiftUI integration path: wrapping
SparkScanViewdirectly withUIViewRepresentableand a coordinator, as an alternative to the existingwithSparkScanview modifier.Part of SDC-31151.
Structure
The new section is placed at the bottom of the page, below the existing
UIViewControllerRepresentable-based content, and follows the page's existing style (step-by-step sections, small code chunks, API links):isActiveflag instead ofviewWillAppear/viewWillDisappear; hit-test forwarding for the floating UISparkScanListenerandSparkScanFeedbackDelegateto SwiftUI closures (with the threading contracts called out)SparkScanSettingsproperty can be forwarded the same wayThe intro notes the trade-off:
withSparkScanis often the easier path (especially for more advanced configurations), whileUIViewRepresentablecan feel more natural in a pure SwiftUI codebase.Notes
ListBuildingSampleSwiftUIRepresentable, in review fordata-capture-sdk).docs/(next version) only; versioned docs untouched.npm run buildpasses; the section renders under thenextversion route.🤖 Generated with Claude Code