Untitled

 avatar
unknown
plain_text
5 months ago
3.3 kB
8
Indexable
//
//  QRCodeScannerView.swift
//  PocketCaddy
//
//  Created by Dylan Anderson on 10/12/23.
//

import SwiftUI
import AVFoundation

final class CaptureSessionManager: ObservableObject {
	var captureSession: AVCaptureSession?
	
	init() {
		captureSession = AVCaptureSession()
	}
}

struct QRCodeScannerView<T>: UIViewControllerRepresentable {
	@ObservedObject var captureSessionManager = CaptureSessionManager()
	var didFindCode: (T?) -> Void
	var onError: (Error) -> Void
	
	class Coordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate {
		var parent: QRCodeScannerView
		
		init(parent: QRCodeScannerView) {
			self.parent = parent
		}
		
		func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
			if let metadataObject = metadataObjects.first,
			   let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject,
			   let stringValue = readableObject.stringValue {
				AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
				parent.didFindCode(stringValue as? T)
				parent.captureSessionManager.captureSession?.stopRunning()
			}
		}
	}
	
	func makeCoordinator() -> Coordinator {
		return Coordinator(parent: self)
	}
	
	func makeUIViewController(context: Context) -> UIViewController {
		let viewController = UIViewController()
		
		guard let videoCaptureDevice = AVCaptureDevice.default(for: .video),
			  let captureSession = captureSessionManager.captureSession else {
			onError(QRCodeScannerError.noCamera)
			return viewController
		}
		
		do {
			let videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice)
			
			if captureSession.canAddInput(videoInput) {
				captureSession.addInput(videoInput)
			} else {
				onError(QRCodeScannerError.videoInputInitFail)
				return viewController
			}
		} catch {
			onError(error)
			return viewController
		}
		
		let metadataOutput = AVCaptureMetadataOutput()
		
		if captureSession.canAddOutput(metadataOutput) {
			captureSession.addOutput(metadataOutput)
			
			metadataOutput.setMetadataObjectsDelegate(context.coordinator, queue: DispatchQueue.main)
			metadataOutput.metadataObjectTypes = [.qr]
		} else {
			onError(QRCodeScannerError.outputInitFail)
			return viewController
		}
		
		let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
		previewLayer.frame = viewController.view.layer.bounds
		previewLayer.videoGravity = .resizeAspectFill
		viewController.view.layer.addSublayer(previewLayer)
		
		DispatchQueue.global(qos: .userInitiated).async {
			captureSession.startRunning()
		}
		
		return viewController
	}
	
	func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
		DispatchQueue.global(qos: .userInitiated).async {
			if let captureSession = self.captureSessionManager.captureSession,
			   !captureSession.isRunning {
				captureSession.startRunning()
			}
		}
	}
}

extension QRCodeScannerView {
	enum QRCodeScannerError: LocalizedError {
		case noCamera
		case videoInputInitFail
		case outputInitFail
		
		var errorDescription: String? {
			switch self {
				case .noCamera:
					return "No camera available"
				case .videoInputInitFail:
					return "Video input initialization failed"
				case .outputInitFail:
					return "Output initialization failed"
			}
		}
	}
}
Editor is loading...
Leave a Comment