Skip to content

Add UIViewRepresentable integration to the SparkScan SwiftUI guide#381

Open
ts1mafei wants to merge 4 commits into
mainfrom
sparkscan-swiftui-uiviewrepresentable-guide
Open

Add UIViewRepresentable integration to the SparkScan SwiftUI guide#381
ts1mafei wants to merge 4 commits into
mainfrom
sparkscan-swiftui-uiviewrepresentable-guide

Conversation

@ts1mafei

@ts1mafei ts1mafei commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

What

Expands the iOS SparkScan Get Started With SwiftUI guide with a new section documenting the pure-SwiftUI integration path: wrapping SparkScanView directly with UIViewRepresentable and a coordinator, as an alternative to the existing withSparkScan view 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):

  • Create the UIViewRepresentable — the wrapper struct; lifecycle driven by an isActive flag instead of viewWillAppear/viewWillDisappear; hit-test forwarding for the floating UI
  • Create the Coordinator — owns the long-lived SparkScan objects; forwards SparkScanListener and SparkScanFeedbackDelegate to SwiftUI closures (with the threading contracts called out)
  • Update Settings While Scanning — diff-and-apply on the existing settings instance so other customizations are preserved; notes that any SparkScanSettings property can be forwarded the same way
  • Use the Scanner in Your SwiftUI App — state-driven usage with a symbology toggle example

The intro notes the trade-off: withSparkScan is often the easier path (especially for more advanced configurations), while UIViewRepresentable can feel more natural in a pure SwiftUI codebase.

Notes

  • The code mirrors a sample implementation that compiles against the current SDK (ListBuildingSampleSwiftUIRepresentable, in review for data-capture-sdk).
  • Edited docs/ (next version) only; versioned docs untouched.
  • npm run build passes; the section renders under the next version route.

🤖 Generated with Claude Code

T1mofi and others added 3 commits July 3, 2026 16:51
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>
@ts1mafei ts1mafei marked this pull request as ready for review July 3, 2026 15:54

@marcozoffoli-scandit marcozoffoli-scandit left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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()
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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()
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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? {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants