Last active
June 14, 2024 19:40
-
-
Save WunDaii/497d44da694d4a013d378df5e47977be to your computer and use it in GitHub Desktop.
Barcode Scanner Camera View for SwiftUI
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// BarcodeScannerViewRepresentable.swift | |
// | |
// Created by Daven Gomes on 28/08/2020. | |
// | |
import SwiftUI | |
import AVFoundation | |
import UIKit | |
struct BarcodeScannerViewRepresentable: UIViewRepresentable { | |
enum ScanningState { | |
case closed | |
case cameraLoading | |
case cameraLoaded | |
case searching | |
case failure | |
} | |
@Binding var currentPosition: AVCaptureDevice.Position | |
@Binding var scanningState: BarcodeScannerViewRepresentable.ScanningState | |
let onBarcodeScanned: (String) -> () | |
class Coordinator: BarcodeScannerUIViewDelegate { | |
@Binding var scanningState: BarcodeScannerViewRepresentable.ScanningState | |
let onBarcodeScanned: (String) -> () | |
init(scanningState: Binding<BarcodeScannerViewRepresentable.ScanningState>, | |
onBarcodeScanned: @escaping (String) -> ()) { | |
self._scanningState = scanningState | |
self.onBarcodeScanned = onBarcodeScanned | |
} | |
func barcodeScanningDidFail() { | |
print("Failed to scan barcode.") | |
} | |
func barcodeScanningDidStop() { | |
print("Stopped scanning barcode.") | |
} | |
func barcodeScanningSucceededWithCode(_ barcode: String) { | |
onBarcodeScanned(barcode) | |
} | |
func cameraLoaded() { | |
withAnimation { | |
scanningState = .cameraLoaded | |
} | |
} | |
func cameraUnloaded() { | |
withAnimation { | |
scanningState = .cameraLoading | |
} | |
} | |
} | |
func makeCoordinator() -> BarcodeScannerViewRepresentable.Coordinator { | |
return Coordinator(scanningState: $scanningState, | |
onBarcodeScanned: onBarcodeScanned) | |
} | |
func makeUIView(context: UIViewRepresentableContext<BarcodeScannerViewRepresentable>) -> BarcodeScannerUIView { | |
let view = BarcodeScannerUIView(frame: .zero) | |
view.delegate = context.coordinator | |
view.backgroundColor = .clear | |
return view | |
} | |
func updateUIView(_ uiView: BarcodeScannerUIView, context: UIViewRepresentableContext<BarcodeScannerViewRepresentable>) { | |
uiView.updateCamera(with: currentPosition) | |
uiView.backgroundColor = .clear | |
} | |
} | |
protocol BarcodeScannerUIViewDelegate: class { | |
func barcodeScanningDidFail() | |
func barcodeScanningSucceededWithCode(_ barcode: String) | |
func barcodeScanningDidStop() | |
func cameraLoaded() | |
func cameraUnloaded() | |
} | |
/// BarcodeScannerUIView - adapted from https://gist.github.com/abhimuralidharan/765ecaf95cbae67de81236b0786bf59f | |
class BarcodeScannerUIView: UIView { | |
weak var delegate: BarcodeScannerUIViewDelegate? | |
private let cameraLoadingDelay: Double = 0.5 // The delay for the AVCaptureVideoPreviewLayer to load. | |
/// capture settion which allows us to start and stop scanning. | |
var captureSession: AVCaptureSession? | |
// Init methods.. | |
required init?(coder aDecoder: NSCoder) { | |
super.init(coder: aDecoder) | |
doInitialSetup() | |
} | |
override init(frame: CGRect) { | |
super.init(frame: frame) | |
doInitialSetup() | |
} | |
//MARK: overriding the layerClass to return `AVCaptureVideoPreviewLayer`. | |
override class var layerClass: AnyClass { | |
return AVCaptureVideoPreviewLayer.self | |
} | |
override var layer: AVCaptureVideoPreviewLayer { | |
return super.layer as! AVCaptureVideoPreviewLayer | |
} | |
var isRunning: Bool { | |
return captureSession?.isRunning ?? false | |
} | |
func startScanning() { | |
captureSession?.startRunning() | |
} | |
func stopScanning() { | |
DispatchQueue.global().async { | |
self.captureSession?.stopRunning() | |
self.captureSession = nil | |
self.layer.session = nil | |
DispatchQueue.main.async { | |
self.delegate?.barcodeScanningDidStop() | |
} | |
} | |
} | |
func updateCamera(with position: AVCaptureDevice.Position) { | |
func cameraWithPosition(position: AVCaptureDevice.Position) -> AVCaptureDevice? { | |
let discoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: AVMediaType.video, position: position) | |
return discoverySession.devices.first { $0.position == position } | |
} | |
guard let captureSession = captureSession else { | |
return | |
} | |
guard let currentCameraInput = captureSession.inputs.first as? AVCaptureDeviceInput else { | |
return | |
} | |
guard currentCameraInput.device.position != position else { | |
return | |
} | |
captureSession.beginConfiguration() | |
captureSession.removeInput(currentCameraInput) | |
guard let newCamera = cameraWithPosition(position: position) else { | |
return | |
} | |
do { | |
let newVideoInput = try AVCaptureDeviceInput(device: newCamera) | |
captureSession.addInput(newVideoInput) | |
captureSession.commitConfiguration() | |
} catch { | |
// Handle this error | |
} | |
} | |
/// Does the initial setup for captureSession | |
private func doInitialSetup() { | |
self.clipsToBounds = true | |
DispatchQueue.global().async { | |
let captureSession = AVCaptureSession() | |
captureSession.startRunning() | |
guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else { return } | |
let videoInput: AVCaptureDeviceInput | |
do { | |
videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice) | |
} catch let error { | |
print(error) | |
return | |
} | |
if captureSession.canAddInput(videoInput) { | |
captureSession.addInput(videoInput) | |
} else { | |
self.scanningDidFail() | |
return | |
} | |
let metadataOutput = AVCaptureMetadataOutput() | |
if captureSession.canAddOutput(metadataOutput) { | |
captureSession.addOutput(metadataOutput) | |
metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main) | |
metadataOutput.metadataObjectTypes = [.ean8, .ean13, .pdf417] | |
} else { | |
self.scanningDidFail() | |
return | |
} | |
self.captureSession = captureSession | |
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { | |
self.layer.opacity = 0 | |
self.layer.session = self.captureSession | |
self.layer.videoGravity = .resizeAspectFill | |
UIView.animate(withDuration: self.cameraLoadingDelay) { | |
self.layer.opacity = 1 | |
self.delegate?.cameraLoaded() | |
} | |
} | |
} | |
self.layer.backgroundColor = UIColor.clear.cgColor | |
backgroundColor = .clear | |
} | |
func scanningDidFail() { | |
delegate?.barcodeScanningDidFail() | |
captureSession = nil | |
} | |
func found(code: String) { | |
delegate?.barcodeScanningSucceededWithCode(code) | |
} | |
} | |
extension BarcodeScannerUIView: AVCaptureMetadataOutputObjectsDelegate { | |
func metadataOutput(_ output: AVCaptureMetadataOutput, | |
didOutput metadataObjects: [AVMetadataObject], | |
from connection: AVCaptureConnection) { | |
guard let metadataObject = metadataObjects.first, | |
let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject, | |
let stringValue = readableObject.stringValue else { return } | |
found(code: stringValue) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment