@cleanuidev/react-native-scanner 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +609 -0
- package/Scanner.podspec +20 -0
- package/android/build.gradle +90 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +8 -0
- package/android/src/main/java/com/scanner/CameraInfoModule.kt +253 -0
- package/android/src/main/java/com/scanner/ScannerPackage.kt +21 -0
- package/android/src/main/java/com/scanner/ScannerView.kt +783 -0
- package/android/src/main/java/com/scanner/ScannerViewManager.kt +181 -0
- package/android/src/main/java/com/scanner/utils/BarcodeFrameManager.kt +170 -0
- package/android/src/main/java/com/scanner/views/BarcodeFrameOverlayView.kt +43 -0
- package/android/src/main/java/com/scanner/views/FocusAreaView.kt +124 -0
- package/ios/BarcodeDetectionManager.swift +229 -0
- package/ios/BarcodeFrameManager.swift +175 -0
- package/ios/BarcodeFrameOverlayView.swift +102 -0
- package/ios/CameraManager.swift +396 -0
- package/ios/CoordinateTransformer.swift +140 -0
- package/ios/FocusAreaOverlayView.swift +161 -0
- package/ios/Models.swift +341 -0
- package/ios/Protocols.swift +194 -0
- package/ios/ScannerView.h +14 -0
- package/ios/ScannerView.mm +358 -0
- package/ios/ScannerViewImpl.swift +580 -0
- package/ios/react-native-scanner-Bridging-Header.h +26 -0
- package/lib/module/CameraInfoModule.js +8 -0
- package/lib/module/CameraInfoModule.js.map +1 -0
- package/lib/module/ScannerViewNativeComponent.ts +121 -0
- package/lib/module/hooks/useCameraInfo.js +106 -0
- package/lib/module/hooks/useCameraInfo.js.map +1 -0
- package/lib/module/index.js +13 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +47 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/CameraInfoModule.d.ts +8 -0
- package/lib/typescript/src/CameraInfoModule.d.ts.map +1 -0
- package/lib/typescript/src/ScannerViewNativeComponent.d.ts +91 -0
- package/lib/typescript/src/ScannerViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useCameraInfo.d.ts +25 -0
- package/lib/typescript/src/hooks/useCameraInfo.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +8 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +145 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/package.json +178 -0
- package/src/CameraInfoModule.ts +11 -0
- package/src/ScannerViewNativeComponent.ts +121 -0
- package/src/hooks/useCameraInfo.ts +190 -0
- package/src/index.tsx +30 -0
- package/src/types.ts +177 -0
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
//
|
|
2
|
+
// ScannerViewImpl.swift
|
|
3
|
+
// react-native-scanner
|
|
4
|
+
//
|
|
5
|
+
// Main scanner view implementation
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import UIKit
|
|
9
|
+
import AVFoundation
|
|
10
|
+
import Vision
|
|
11
|
+
|
|
12
|
+
/// Main scanner view that manages camera, detection, and overlays
|
|
13
|
+
@objc(ScannerViewImpl)
|
|
14
|
+
@objcMembers
|
|
15
|
+
class ScannerViewImpl: UIView {
|
|
16
|
+
|
|
17
|
+
// MARK: - Public Properties
|
|
18
|
+
|
|
19
|
+
/// Delegate for scanner events
|
|
20
|
+
@objc public weak var delegate: ScannerViewDelegate?
|
|
21
|
+
|
|
22
|
+
// MARK: - Private Properties
|
|
23
|
+
|
|
24
|
+
/// Camera manager for AVFoundation operations
|
|
25
|
+
private let cameraManager: CameraManager
|
|
26
|
+
|
|
27
|
+
/// Barcode detection manager for Vision framework operations
|
|
28
|
+
private let barcodeDetectionManager: BarcodeDetectionManager
|
|
29
|
+
|
|
30
|
+
/// Frame manager for barcode frame lifecycle
|
|
31
|
+
private let frameManager: BarcodeFrameManager
|
|
32
|
+
|
|
33
|
+
/// Overlay view for focus area
|
|
34
|
+
private let focusAreaOverlay: FocusAreaOverlayView
|
|
35
|
+
|
|
36
|
+
/// Overlay view for barcode frames
|
|
37
|
+
private let barcodeFrameOverlay: BarcodeFrameOverlayView
|
|
38
|
+
|
|
39
|
+
/// Reference to the preview layer
|
|
40
|
+
private var previewLayer: AVCaptureVideoPreviewLayer?
|
|
41
|
+
|
|
42
|
+
// Configuration state
|
|
43
|
+
private var focusAreaConfig: FocusAreaConfig
|
|
44
|
+
private var barcodeFramesConfig: BarcodeFramesConfig
|
|
45
|
+
private var scanStrategy: BarcodeScanStrategy
|
|
46
|
+
private var isPaused: Bool
|
|
47
|
+
private var keepScreenOn: Bool
|
|
48
|
+
private var isPreviewLayerAdded: Bool = false
|
|
49
|
+
|
|
50
|
+
// Remember torch state so we can re-apply after camera starts
|
|
51
|
+
private var requestedTorchEnabled: Bool = false
|
|
52
|
+
|
|
53
|
+
// Track visibility so we can stop the camera when this view isn't on-screen (no JS hooks needed)
|
|
54
|
+
private var isViewVisible: Bool = false
|
|
55
|
+
|
|
56
|
+
// Debounce mechanism to prevent multiple rapid barcode detections
|
|
57
|
+
private var lastBarcodeEmissionTime: TimeInterval = 0
|
|
58
|
+
private var barcodeEmissionDebounceInterval: TimeInterval = 0.5 // Default 500ms debounce
|
|
59
|
+
|
|
60
|
+
// MARK: - Initialization
|
|
61
|
+
|
|
62
|
+
@objc override init(frame: CGRect) {
|
|
63
|
+
// Initialize managers and configuration
|
|
64
|
+
self.cameraManager = CameraManager()
|
|
65
|
+
self.barcodeDetectionManager = BarcodeDetectionManager()
|
|
66
|
+
self.frameManager = BarcodeFrameManager()
|
|
67
|
+
self.focusAreaOverlay = FocusAreaOverlayView()
|
|
68
|
+
self.barcodeFrameOverlay = BarcodeFrameOverlayView()
|
|
69
|
+
|
|
70
|
+
// Initialize configuration with defaults
|
|
71
|
+
self.focusAreaConfig = .defaultConfig
|
|
72
|
+
self.barcodeFramesConfig = .defaultConfig
|
|
73
|
+
self.scanStrategy = .defaultStrategy
|
|
74
|
+
self.isPaused = false
|
|
75
|
+
self.keepScreenOn = true
|
|
76
|
+
|
|
77
|
+
super.init(frame: frame)
|
|
78
|
+
|
|
79
|
+
print("[ScannerViewImpl] 🚀 Initializing ScannerViewImpl with frame: \(frame)")
|
|
80
|
+
setupView()
|
|
81
|
+
setupDelegates()
|
|
82
|
+
setupCamera()
|
|
83
|
+
print("[ScannerViewImpl] ✅ ScannerViewImpl initialization complete")
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@objc convenience init(frame: CGRect, delegate: ScannerViewDelegate?) {
|
|
87
|
+
self.init(frame: frame)
|
|
88
|
+
self.delegate = delegate
|
|
89
|
+
print("[ScannerViewImpl] ✅ Delegate set via convenience initializer")
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
required init?(coder: NSCoder) {
|
|
93
|
+
fatalError("init(coder:) has not been implemented")
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// MARK: - Setup Methods
|
|
97
|
+
|
|
98
|
+
/// Setup the view hierarchy and layout
|
|
99
|
+
private func setupView() {
|
|
100
|
+
backgroundColor = .black
|
|
101
|
+
|
|
102
|
+
// Add subviews in order (bottom to top)
|
|
103
|
+
addSubview(focusAreaOverlay)
|
|
104
|
+
addSubview(barcodeFrameOverlay)
|
|
105
|
+
|
|
106
|
+
// Configure autoresizing masks
|
|
107
|
+
focusAreaOverlay.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
108
|
+
barcodeFrameOverlay.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
109
|
+
|
|
110
|
+
print("[ScannerViewImpl] View hierarchy setup complete")
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// Setup delegates for all managers
|
|
114
|
+
private func setupDelegates() {
|
|
115
|
+
cameraManager.delegate = self
|
|
116
|
+
barcodeDetectionManager.delegate = self
|
|
117
|
+
frameManager.delegate = self
|
|
118
|
+
|
|
119
|
+
// Setup frame manager callback
|
|
120
|
+
frameManager.onFramesChanged = { [weak self] frames in
|
|
121
|
+
self?.barcodeFrameOverlay.setBarcodeBoxes(frames)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
print("[ScannerViewImpl] Delegates configured")
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/// Setup the camera and start preview
|
|
128
|
+
private func setupCamera() {
|
|
129
|
+
// Don't start camera here. Fabric/Navigation may create the view before it is on-screen.
|
|
130
|
+
// We'll start/stop based on actual visibility in `updateVisibility`.
|
|
131
|
+
print("[ScannerViewImpl] 📷 Camera will start when view becomes visible")
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// MARK: - Public Configuration Methods
|
|
135
|
+
|
|
136
|
+
/// Set barcode formats to detect
|
|
137
|
+
/// - Parameter formats: Array of format strings
|
|
138
|
+
@objc func setBarcodeTypes(_ formats: [String]) {
|
|
139
|
+
let barcodeFormats = formats.compactMap { BarcodeFormat(rawValue: $0) }
|
|
140
|
+
barcodeDetectionManager.setBarcodeFormats(barcodeFormats)
|
|
141
|
+
print("[ScannerViewImpl] Barcode types set: \(formats)")
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/// Configure focus area
|
|
145
|
+
/// - Parameter config: Focus area configuration dictionary
|
|
146
|
+
@objc func configureFocusArea(_ config: [String: Any]) {
|
|
147
|
+
focusAreaConfig = FocusAreaConfig.from(dict: config)
|
|
148
|
+
focusAreaOverlay.updateFocusArea(config: focusAreaConfig)
|
|
149
|
+
print("[ScannerViewImpl] Focus area configured")
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/// Configure barcode frames
|
|
153
|
+
/// - Parameter config: Barcode frames configuration dictionary
|
|
154
|
+
@objc func configureBarcodeFrames(_ config: [String: Any]) {
|
|
155
|
+
barcodeFramesConfig = BarcodeFramesConfig.from(dict: config)
|
|
156
|
+
barcodeFrameOverlay.updateBarcodeFrames(config: barcodeFramesConfig)
|
|
157
|
+
print("[ScannerViewImpl] Barcode frames configured")
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/// Set torch enabled/disabled
|
|
161
|
+
/// - Parameter enabled: Whether torch should be enabled
|
|
162
|
+
@objc func setTorchEnabled(_ enabled: NSNumber) {
|
|
163
|
+
let value = enabled.boolValue
|
|
164
|
+
print("[ScannerViewImpl] Torch requested: \(value)")
|
|
165
|
+
requestedTorchEnabled = value
|
|
166
|
+
cameraManager.setTorch(enabled: value)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/// Set zoom level
|
|
170
|
+
/// - Parameter zoom: The zoom level
|
|
171
|
+
@objc func setZoomLevel(_ zoom: NSNumber) {
|
|
172
|
+
cameraManager.setZoom(level: CGFloat(zoom.doubleValue))
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/// Pause or resume scanning
|
|
176
|
+
/// - Parameter paused: Whether scanning should be paused
|
|
177
|
+
@objc func setPauseScanning(_ paused: NSNumber) {
|
|
178
|
+
let value = paused.boolValue
|
|
179
|
+
isPaused = value
|
|
180
|
+
if value {
|
|
181
|
+
barcodeDetectionManager.pauseScanning()
|
|
182
|
+
frameManager.clearAllFrames()
|
|
183
|
+
// Reset debounce timer when pausing to prevent stale emissions
|
|
184
|
+
lastBarcodeEmissionTime = 0
|
|
185
|
+
} else {
|
|
186
|
+
barcodeDetectionManager.resumeScanning()
|
|
187
|
+
// Reset debounce timer when resuming to allow immediate detection
|
|
188
|
+
lastBarcodeEmissionTime = 0
|
|
189
|
+
}
|
|
190
|
+
print("[ScannerViewImpl] Scanning \(value ? "paused" : "resumed")")
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/// Set scan strategy
|
|
194
|
+
/// - Parameter strategy: Strategy name as string
|
|
195
|
+
@objc func setBarcodeScanStrategy(_ strategy: String) {
|
|
196
|
+
if let scanStrat = BarcodeScanStrategy(rawValue: strategy) {
|
|
197
|
+
scanStrategy = scanStrat
|
|
198
|
+
barcodeDetectionManager.setScanStrategy(scanStrat)
|
|
199
|
+
print("[ScannerViewImpl] Scan strategy set to: \(strategy)")
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/// Set keep screen on
|
|
204
|
+
/// - Parameter keepOn: Whether to keep screen on
|
|
205
|
+
@objc func setKeepScreenOn(_ keepOn: NSNumber) {
|
|
206
|
+
let value = keepOn.boolValue
|
|
207
|
+
keepScreenOn = value
|
|
208
|
+
UIApplication.shared.isIdleTimerDisabled = value
|
|
209
|
+
print("[ScannerViewImpl] Keep screen on: \(value)")
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/// Set barcode emission debounce interval
|
|
213
|
+
/// - Parameter interval: Minimum interval in seconds between barcode emissions (0 to disable)
|
|
214
|
+
@objc func setBarcodeEmissionInterval(_ interval: NSNumber) {
|
|
215
|
+
let value = interval.doubleValue
|
|
216
|
+
barcodeEmissionDebounceInterval = max(0.0, value) // Ensure non-negative
|
|
217
|
+
print("[ScannerViewImpl] Barcode emission interval set to: \(barcodeEmissionDebounceInterval)s")
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// MARK: - Lifecycle Methods
|
|
221
|
+
|
|
222
|
+
override func layoutSubviews() {
|
|
223
|
+
super.layoutSubviews()
|
|
224
|
+
|
|
225
|
+
print("[ScannerViewImpl] 📐 layoutSubviews called with bounds: \(bounds)")
|
|
226
|
+
|
|
227
|
+
// Add preview layer if we have a valid frame and haven't added it yet
|
|
228
|
+
if !isPreviewLayerAdded && bounds.width > 0 && bounds.height > 0 {
|
|
229
|
+
if let preview = cameraManager.getPreviewLayer() {
|
|
230
|
+
self.previewLayer = preview
|
|
231
|
+
preview.frame = bounds
|
|
232
|
+
layer.insertSublayer(preview, at: 0)
|
|
233
|
+
isPreviewLayerAdded = true
|
|
234
|
+
print("[ScannerViewImpl] ✅ Preview layer added in layoutSubviews with frame: \(bounds)")
|
|
235
|
+
} else {
|
|
236
|
+
print("[ScannerViewImpl] ⚠️ Preview layer not ready yet in layoutSubviews")
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Update preview layer frame if already added
|
|
241
|
+
if isPreviewLayerAdded {
|
|
242
|
+
previewLayer?.frame = bounds
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Update overlay frames
|
|
246
|
+
focusAreaOverlay.frame = bounds
|
|
247
|
+
barcodeFrameOverlay.frame = bounds
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
override func didMoveToWindow() {
|
|
251
|
+
super.didMoveToWindow()
|
|
252
|
+
|
|
253
|
+
let nowVisible = (self.window != nil) && !self.isHidden && self.alpha > 0.01
|
|
254
|
+
updateVisibility(nowVisible)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
override func didMoveToSuperview() {
|
|
258
|
+
super.didMoveToSuperview()
|
|
259
|
+
let nowVisible = (self.window != nil) && !self.isHidden && self.alpha > 0.01
|
|
260
|
+
updateVisibility(nowVisible)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
override var isHidden: Bool {
|
|
264
|
+
didSet {
|
|
265
|
+
let nowVisible = (self.window != nil) && !self.isHidden && self.alpha > 0.01
|
|
266
|
+
updateVisibility(nowVisible)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
override var alpha: CGFloat {
|
|
271
|
+
didSet {
|
|
272
|
+
let nowVisible = (self.window != nil) && !self.isHidden && self.alpha > 0.01
|
|
273
|
+
updateVisibility(nowVisible)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private func updateVisibility(_ visible: Bool) {
|
|
278
|
+
guard visible != isViewVisible else { return }
|
|
279
|
+
isViewVisible = visible
|
|
280
|
+
|
|
281
|
+
if visible {
|
|
282
|
+
print("[ScannerViewImpl] 👁️ View became visible -> starting camera")
|
|
283
|
+
if keepScreenOn {
|
|
284
|
+
UIApplication.shared.isIdleTimerDisabled = true
|
|
285
|
+
}
|
|
286
|
+
cameraManager.startCamera()
|
|
287
|
+
} else {
|
|
288
|
+
print("[ScannerViewImpl] 🙈 View not visible -> stopping camera")
|
|
289
|
+
cameraManager.stopCamera()
|
|
290
|
+
// release keep-screen-on when off-screen
|
|
291
|
+
UIApplication.shared.isIdleTimerDisabled = false
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
deinit {
|
|
296
|
+
cameraManager.stopCamera()
|
|
297
|
+
frameManager.shutdown()
|
|
298
|
+
UIApplication.shared.isIdleTimerDisabled = false
|
|
299
|
+
print("[ScannerViewImpl] Deinitialized")
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// MARK: - Private Helper Methods
|
|
303
|
+
|
|
304
|
+
/// Process detected barcodes according to strategy and filters
|
|
305
|
+
/// - Parameter observations: Raw barcode observations from Vision
|
|
306
|
+
/// - Returns: Filtered and processed barcode results
|
|
307
|
+
private func processBarcodeObservations(_ observations: [VNBarcodeObservation]) -> [BarcodeDetectionResult] {
|
|
308
|
+
guard !observations.isEmpty else { return [] }
|
|
309
|
+
|
|
310
|
+
// Step 1: Filter by focus area if enabled
|
|
311
|
+
let filteredObservations = filterByFocusArea(observations)
|
|
312
|
+
guard !filteredObservations.isEmpty else { return [] }
|
|
313
|
+
|
|
314
|
+
// Step 2: Apply scan strategy
|
|
315
|
+
let strategyObservations = applyScanStrategy(filteredObservations)
|
|
316
|
+
guard !strategyObservations.isEmpty else { return [] }
|
|
317
|
+
|
|
318
|
+
// Step 3: Transform coordinates and create results
|
|
319
|
+
var results: [BarcodeDetectionResult] = []
|
|
320
|
+
for observation in strategyObservations {
|
|
321
|
+
// Transform bounding box from Vision to View coordinates
|
|
322
|
+
let viewRect = CoordinateTransformer.transformVisionRectToViewRect(
|
|
323
|
+
observation.boundingBox,
|
|
324
|
+
viewSize: bounds.size,
|
|
325
|
+
previewLayer: previewLayer
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
// Get the barcode format string
|
|
329
|
+
let formatString = observation.symbology.rawValue
|
|
330
|
+
|
|
331
|
+
// Create result
|
|
332
|
+
if let result = BarcodeDetectionResult.from(
|
|
333
|
+
observation: observation,
|
|
334
|
+
boundingBox: viewRect,
|
|
335
|
+
format: formatString
|
|
336
|
+
) {
|
|
337
|
+
results.append(result)
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return results
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/// Filter barcodes by focus area if enabled
|
|
345
|
+
/// - Parameter observations: Barcode observations to filter
|
|
346
|
+
/// - Returns: Filtered observations
|
|
347
|
+
private func filterByFocusArea(_ observations: [VNBarcodeObservation]) -> [VNBarcodeObservation] {
|
|
348
|
+
guard focusAreaConfig.enabled, let focusRect = focusAreaOverlay.getFocusAreaFrame() else {
|
|
349
|
+
return observations
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return observations.filter { observation in
|
|
353
|
+
// Transform barcode rect to view coordinates
|
|
354
|
+
let viewRect = CoordinateTransformer.transformVisionRectToViewRect(
|
|
355
|
+
observation.boundingBox,
|
|
356
|
+
viewSize: bounds.size,
|
|
357
|
+
previewLayer: previewLayer
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
// Check if barcode is within focus area
|
|
361
|
+
return focusRect.contains(viewRect)
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/// Apply scan strategy to barcode observations
|
|
366
|
+
/// - Parameter observations: Barcode observations
|
|
367
|
+
/// - Returns: Processed observations according to strategy
|
|
368
|
+
private func applyScanStrategy(_ observations: [VNBarcodeObservation]) -> [VNBarcodeObservation] {
|
|
369
|
+
guard !observations.isEmpty else { return [] }
|
|
370
|
+
|
|
371
|
+
switch scanStrategy {
|
|
372
|
+
case .one:
|
|
373
|
+
// Return only the first barcode
|
|
374
|
+
return [observations[0]]
|
|
375
|
+
|
|
376
|
+
case .all:
|
|
377
|
+
// Return all barcodes
|
|
378
|
+
return observations
|
|
379
|
+
|
|
380
|
+
case .biggest:
|
|
381
|
+
// Return only the largest barcode by area
|
|
382
|
+
let observationsWithArea = observations.compactMap { observation -> (VNBarcodeObservation, CGFloat)? in
|
|
383
|
+
let rect = observation.boundingBox
|
|
384
|
+
let area = rect.width * rect.height
|
|
385
|
+
return (observation, area)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if let biggest = observationsWithArea.max(by: { $0.1 < $1.1 }) {
|
|
389
|
+
return [biggest.0]
|
|
390
|
+
}
|
|
391
|
+
return []
|
|
392
|
+
|
|
393
|
+
case .sortByBiggest:
|
|
394
|
+
// Sort all barcodes by area (largest first)
|
|
395
|
+
let observationsWithArea = observations.compactMap { observation -> (VNBarcodeObservation, CGFloat)? in
|
|
396
|
+
let rect = observation.boundingBox
|
|
397
|
+
let area = rect.width * rect.height
|
|
398
|
+
return (observation, area)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return observationsWithArea
|
|
402
|
+
.sorted { $0.1 > $1.1 }
|
|
403
|
+
.map { $0.0 }
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/// Update barcode frame display
|
|
408
|
+
/// - Parameter observations: Barcode observations to display
|
|
409
|
+
private func updateBarcodeFrameDisplay(_ observations: [VNBarcodeObservation]) {
|
|
410
|
+
guard barcodeFramesConfig.enabled else {
|
|
411
|
+
frameManager.clearAllFrames()
|
|
412
|
+
return
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
let focusRect = focusAreaOverlay.getFocusAreaFrame()
|
|
416
|
+
|
|
417
|
+
var frameDict: [String: CGRect] = [:]
|
|
418
|
+
|
|
419
|
+
for observation in observations {
|
|
420
|
+
guard let barcodeValue = observation.payloadStringValue else { continue }
|
|
421
|
+
|
|
422
|
+
// Transform to view coordinates
|
|
423
|
+
let viewRect = CoordinateTransformer.transformVisionRectToViewRect(
|
|
424
|
+
observation.boundingBox,
|
|
425
|
+
viewSize: bounds.size,
|
|
426
|
+
previewLayer: previewLayer
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
// Filter by focus area if onlyInFocusArea is enabled
|
|
430
|
+
if barcodeFramesConfig.onlyInFocusArea, let focusRect {
|
|
431
|
+
if focusRect.contains(viewRect) {
|
|
432
|
+
frameDict[barcodeValue] = viewRect
|
|
433
|
+
}
|
|
434
|
+
} else {
|
|
435
|
+
frameDict[barcodeValue] = viewRect
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
frameManager.updateBarcodeFrames(frameDict)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/// Emit barcodes detected event to React Native
|
|
443
|
+
/// - Parameter results: Detected barcode results
|
|
444
|
+
private func emitBarcodesDetected(_ results: [BarcodeDetectionResult]) {
|
|
445
|
+
// Debounce: Prevent rapid duplicate emissions
|
|
446
|
+
let currentTime = Date().timeIntervalSince1970
|
|
447
|
+
let timeSinceLastEmission = currentTime - lastBarcodeEmissionTime
|
|
448
|
+
|
|
449
|
+
// If we've emitted recently (within debounce interval), skip this emission
|
|
450
|
+
// This prevents multiple alerts when pauseScanning is set but detection callbacks are still in flight
|
|
451
|
+
guard timeSinceLastEmission >= barcodeEmissionDebounceInterval else {
|
|
452
|
+
print("[ScannerViewImpl] ⏭️ Skipping barcode emission (debounced, last emission was \(timeSinceLastEmission)s ago)")
|
|
453
|
+
return
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
lastBarcodeEmissionTime = currentTime
|
|
457
|
+
let barcodesArray = results.map { $0.toDictionary() }
|
|
458
|
+
delegate?.scannerDidDetectBarcodes(barcodesArray)
|
|
459
|
+
print("[ScannerViewImpl] ✅ Barcode emitted (debounced)")
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/// Emit error event to React Native
|
|
463
|
+
/// - Parameter error: The error that occurred
|
|
464
|
+
private func emitError(_ error: ScannerError) {
|
|
465
|
+
delegate?.scannerDidEncounterError(error.toDictionary())
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/// Emit load event to React Native
|
|
469
|
+
/// - Parameter success: Whether loading was successful
|
|
470
|
+
/// - Parameter error: Optional error message
|
|
471
|
+
private func emitLoadEvent(success: Bool, error: String? = nil) {
|
|
472
|
+
let payload = LoadEventPayload(success: success, error: error)
|
|
473
|
+
delegate?.scannerDidLoad(payload.toDictionary())
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// MARK: - CameraManagerDelegate
|
|
478
|
+
|
|
479
|
+
extension ScannerViewImpl: CameraManagerDelegate {
|
|
480
|
+
func cameraManager(_ manager: CameraManager, didOutput sampleBuffer: CMSampleBuffer) {
|
|
481
|
+
// Don't process if scanning is paused
|
|
482
|
+
guard !isPaused else { return }
|
|
483
|
+
|
|
484
|
+
// Pass sample buffer to barcode detection
|
|
485
|
+
barcodeDetectionManager.detectBarcodes(in: sampleBuffer) { [weak self] observations in
|
|
486
|
+
guard let self = self, !self.isPaused else { return }
|
|
487
|
+
|
|
488
|
+
// Process observations
|
|
489
|
+
let results = self.processBarcodeObservations(observations)
|
|
490
|
+
|
|
491
|
+
// Update barcode frames display
|
|
492
|
+
self.updateBarcodeFrameDisplay(observations)
|
|
493
|
+
|
|
494
|
+
// Emit results if any
|
|
495
|
+
if !results.isEmpty {
|
|
496
|
+
self.emitBarcodesDetected(results)
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
func cameraManagerDidFail(_ manager: CameraManager, error: Error) {
|
|
502
|
+
print("[ScannerViewImpl] Camera failed: \(error.localizedDescription)")
|
|
503
|
+
let scannerError = ScannerError.from(
|
|
504
|
+
code: .cameraInitializationFailed,
|
|
505
|
+
message: error.localizedDescription
|
|
506
|
+
)
|
|
507
|
+
emitError(scannerError)
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
func cameraManagerDidStart(_ manager: CameraManager) {
|
|
511
|
+
print("[ScannerViewImpl] ✅ Camera started successfully")
|
|
512
|
+
|
|
513
|
+
// Re-apply torch after the session is fully running (fixes "torch set too early" cases)
|
|
514
|
+
manager.setTorch(enabled: requestedTorchEnabled)
|
|
515
|
+
|
|
516
|
+
// Try to add preview layer on main thread
|
|
517
|
+
DispatchQueue.main.async { [weak self] in
|
|
518
|
+
guard let self = self else { return }
|
|
519
|
+
|
|
520
|
+
// Check bounds multiple times with delay to catch layout
|
|
521
|
+
self.attemptToAddPreviewLayer(attempt: 0)
|
|
522
|
+
|
|
523
|
+
self.emitLoadEvent(success: true)
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
private func attemptToAddPreviewLayer(attempt: Int) {
|
|
528
|
+
guard !isPreviewLayerAdded else { return }
|
|
529
|
+
|
|
530
|
+
if self.bounds.width > 0 && self.bounds.height > 0 {
|
|
531
|
+
if let preview = self.cameraManager.getPreviewLayer() {
|
|
532
|
+
self.previewLayer = preview
|
|
533
|
+
preview.frame = self.bounds
|
|
534
|
+
self.layer.insertSublayer(preview, at: 0)
|
|
535
|
+
self.isPreviewLayerAdded = true
|
|
536
|
+
print("[ScannerViewImpl] ✅ Preview layer added with frame: \(self.bounds)")
|
|
537
|
+
}
|
|
538
|
+
} else if attempt < 10 {
|
|
539
|
+
// Try again after a short delay (max 10 attempts = ~2 seconds)
|
|
540
|
+
print("[ScannerViewImpl] ⏳ Attempt \(attempt + 1): Waiting for valid bounds (current: \(self.bounds))")
|
|
541
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
|
|
542
|
+
self?.attemptToAddPreviewLayer(attempt: attempt + 1)
|
|
543
|
+
}
|
|
544
|
+
} else {
|
|
545
|
+
print("[ScannerViewImpl] ❌ Failed to add preview layer after 10 attempts - bounds never became valid")
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// MARK: - BarcodeDetectionDelegate
|
|
551
|
+
|
|
552
|
+
extension ScannerViewImpl: BarcodeDetectionDelegate {
|
|
553
|
+
func barcodeDetectionManager(_ manager: BarcodeDetectionManager,
|
|
554
|
+
didDetect observations: [VNBarcodeObservation]) {
|
|
555
|
+
// This is only called when using the delegate-based detection
|
|
556
|
+
// We're using the completion-based approach in cameraManager:didOutput:
|
|
557
|
+
// But we can handle it here as well if needed
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
func barcodeDetectionManager(_ manager: BarcodeDetectionManager,
|
|
561
|
+
didFailWith error: Error) {
|
|
562
|
+
print("[ScannerViewImpl] Barcode detection failed: \(error.localizedDescription)")
|
|
563
|
+
let scannerError = ScannerError.from(
|
|
564
|
+
code: .barcodeDetectionFailed,
|
|
565
|
+
message: error.localizedDescription
|
|
566
|
+
)
|
|
567
|
+
emitError(scannerError)
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// MARK: - BarcodeFrameManagerDelegate
|
|
572
|
+
|
|
573
|
+
extension ScannerViewImpl: BarcodeFrameManagerDelegate {
|
|
574
|
+
func barcodeFrameManager(_ manager: BarcodeFrameManager,
|
|
575
|
+
didUpdateFrames frames: [CGRect]) {
|
|
576
|
+
// Frames are automatically updated via the closure callback
|
|
577
|
+
// This delegate method is here for additional functionality if needed
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//
|
|
2
|
+
// react-native-scanner-Bridging-Header.h
|
|
3
|
+
// react-native-scanner
|
|
4
|
+
//
|
|
5
|
+
// Bridging header for Objective-C to Swift
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
#ifndef react_native_scanner_Bridging_Header_h
|
|
9
|
+
#define react_native_scanner_Bridging_Header_h
|
|
10
|
+
|
|
11
|
+
// React Native imports
|
|
12
|
+
#import <React/RCTViewComponentView.h>
|
|
13
|
+
#import <React/RCTConversions.h>
|
|
14
|
+
|
|
15
|
+
// UIKit and Foundation
|
|
16
|
+
#import <UIKit/UIKit.h>
|
|
17
|
+
#import <Foundation/Foundation.h>
|
|
18
|
+
|
|
19
|
+
// AVFoundation
|
|
20
|
+
#import <AVFoundation/AVFoundation.h>
|
|
21
|
+
|
|
22
|
+
// Vision Framework
|
|
23
|
+
#import <Vision/Vision.h>
|
|
24
|
+
|
|
25
|
+
#endif /* react_native_scanner_Bridging_Header_h */
|
|
26
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["NativeModules","CameraInfoModule"],"sourceRoot":"../../src","sources":["CameraInfoModule.ts"],"mappings":";;AAAA,SAASA,aAAa,QAAQ,cAAc;AAG5C,MAAM;EAAEC;AAAiB,CAAC,GAAGD,aAAa;AAO1C,eAAeC,gBAAgB","ignoreList":[]}
|