QRScannerUIView.swift 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
  1. import AVFoundation
  2. import SwiftUI
  3. struct QRScannerUIView: UIViewRepresentable {
  4. var onCodeDetected: (String) -> Void
  5. func makeUIView(context: Context) -> some UIView {
  6. let view = QRScannerUIViewContainer()
  7. let captureSession = AVCaptureSession()
  8. guard let videoCaptureDevice = AVCaptureDevice.default(for: .video),
  9. let videoInput = try? AVCaptureDeviceInput(device: videoCaptureDevice),
  10. captureSession.canAddInput(videoInput) else {
  11. return view
  12. }
  13. captureSession.addInput(videoInput)
  14. let metadataOutput = AVCaptureMetadataOutput()
  15. captureSession.addOutput(metadataOutput)
  16. metadataOutput.setMetadataObjectsDelegate(context.coordinator, queue: DispatchQueue.main)
  17. metadataOutput.metadataObjectTypes = [.qr]
  18. let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
  19. previewLayer.frame = view.layer.bounds
  20. previewLayer.videoGravity = .resizeAspectFill
  21. view.layer.insertSublayer(previewLayer, at: 0)
  22. // Move the startRunning call to a background thread
  23. DispatchQueue.global(qos: .userInitiated).async {
  24. captureSession.startRunning()
  25. }
  26. view.previewLayer = previewLayer
  27. view.captureSession = captureSession
  28. return view
  29. }
  30. func updateUIView(_ uiView: UIViewType, context: Context) {}
  31. func makeCoordinator() -> Coordinator {
  32. Coordinator(onCodeDetected: onCodeDetected)
  33. }
  34. class Coordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate {
  35. var onCodeDetected: (String) -> Void
  36. private var lastScanDate: Date?
  37. private let debounceInterval: TimeInterval = 3.0
  38. init(onCodeDetected: @escaping (String) -> Void) {
  39. self.onCodeDetected = onCodeDetected
  40. }
  41. func qrCodeScanned(_ code: String) {
  42. let now = Date()
  43. // If it's the first scan or the interval since the last scan is more than the debounce interval
  44. if let lastScan = lastScanDate, now.timeIntervalSince(lastScan) < debounceInterval {
  45. return
  46. }
  47. onCodeDetected(code)
  48. lastScanDate = now
  49. }
  50. func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
  51. if let metadataObject = metadataObjects.first as? AVMetadataMachineReadableCodeObject, let stringValue = metadataObject.stringValue {
  52. qrCodeScanned(stringValue)
  53. }
  54. }
  55. }
  56. }
  57. class QRScannerUIViewContainer: UIView {
  58. var previewLayer: AVCaptureVideoPreviewLayer?
  59. var captureSession: AVCaptureSession?
  60. override func layoutSubviews() {
  61. super.layoutSubviews()
  62. previewLayer?.frame = self.bounds
  63. }
  64. }