// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import UIKit import AVFoundation import SessionUIKit import SessionUtilitiesKit protocol QRScannerDelegate: AnyObject { func controller(_ controller: QRCodeScanningViewController, didDetectQRCodeWith string: String, onError: (() -> ())?) } class QRCodeScanningViewController: UIViewController, AVCaptureMetadataOutputObjectsDelegate { public weak var scanDelegate: QRScannerDelegate? private let captureQueue: DispatchQueue = DispatchQueue.global(qos: .default) private var capture: AVCaptureSession? private var captureLayer: AVCaptureVideoPreviewLayer? private var captureEnabled: Bool = false // MARK: - Initialization deinit { self.captureLayer?.removeFromSuperlayer() } // MARK: - Components private let maskingView: UIView = UIView() private lazy var maskLayer: CAShapeLayer = { let result: CAShapeLayer = CAShapeLayer() result.fillRule = .evenOdd result.themeFillColor = .black result.opacity = 0.32 return result }() // MARK: - Lifecycle override func loadView() { super.loadView() self.view.addSubview(maskingView) maskingView.layer.addSublayer(maskLayer) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) if captureEnabled { self.startCapture() } } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) self.stopCapture() } override func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() captureLayer?.frame = self.view.bounds if maskingView.frame != self.view.bounds { // Add a circular mask let path: UIBezierPath = UIBezierPath(rect: self.view.bounds) let radius: CGFloat = ((min(self.view.bounds.size.width, self.view.bounds.size.height) * 0.5) - Values.largeSpacing) // Center the circle's bounding rectangle let circleRect: CGRect = CGRect( x: ((self.view.bounds.size.width * 0.5) - radius), y: ((self.view.bounds.size.height * 0.5) - radius), width: (radius * 2), height: (radius * 2) ) let clippingPath: UIBezierPath = UIBezierPath.init( roundedRect: circleRect, cornerRadius: 16 ) path.append(clippingPath) path.usesEvenOddFillRule = true maskLayer.path = path.cgPath } } // MARK: - Functions public func startCapture() { self.captureEnabled = true // Note: The simulator doesn't support video but if we do try to start an // AVCaptureSession it seems to hang on that particular thread indefinitely // this will prevent us from trying to start a session on the simulator #if targetEnvironment(simulator) #else if self.capture == nil { self.captureQueue.async { [weak self] in let maybeDevice: AVCaptureDevice? = { if let result = AVCaptureDevice.default(.builtInDualCamera, for: .video, position: .back) { return result } return AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) }() // Set the input device to autoFocus (since we don't have the interaction setup for // doing it manually) do { try maybeDevice?.lockForConfiguration() maybeDevice?.focusMode = .continuousAutoFocus maybeDevice?.unlockForConfiguration() } catch {} // Device input guard let device: AVCaptureDevice = maybeDevice, let input: AVCaptureInput = try? AVCaptureDeviceInput(device: device) else { return SNLog("Failed to retrieve the device for enabling the QRCode scanning camera") } // Image output let output: AVCaptureVideoDataOutput = AVCaptureVideoDataOutput() output.alwaysDiscardsLateVideoFrames = true // Metadata output the session let metadataOutput: AVCaptureMetadataOutput = AVCaptureMetadataOutput() metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main) let capture: AVCaptureSession = AVCaptureSession() capture.beginConfiguration() if capture.canAddInput(input) { capture.addInput(input) } if capture.canAddOutput(output) { capture.addOutput(output) } if capture.canAddOutput(metadataOutput) { capture.addOutput(metadataOutput) } guard !capture.inputs.isEmpty && capture.outputs.count == 2 else { return SNLog("Failed to attach the input/output to the capture session") } guard metadataOutput.availableMetadataObjectTypes.contains(.qr) else { return SNLog("The output is unable to process QR codes") } // Specify that we want to capture QR Codes (Needs to be done after being added // to the session, 'availableMetadataObjectTypes' is empty beforehand) metadataOutput.metadataObjectTypes = [.qr] capture.commitConfiguration() // Create the layer for rendering the camera video let layer: AVCaptureVideoPreviewLayer = AVCaptureVideoPreviewLayer(session: capture) layer.videoGravity = AVLayerVideoGravity.resizeAspectFill // Start running the capture session capture.startRunning() DispatchQueue.main.async { layer.frame = (self?.view.bounds ?? .zero) self?.view.layer.addSublayer(layer) if let maskingView: UIView = self?.maskingView { self?.view.bringSubviewToFront(maskingView) } self?.capture = capture self?.captureLayer = layer } } } else { self.capture?.startRunning() } #endif } private func stopCapture() { self.captureEnabled = false self.captureQueue.async { [weak self] in self?.capture?.stopRunning() } } func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) { guard self.captureEnabled, let metadata: AVMetadataObject = metadataObjects.first(where: { ($0 as? AVMetadataMachineReadableCodeObject)?.type == .qr }), let qrCodeInfo: AVMetadataMachineReadableCodeObject = metadata as? AVMetadataMachineReadableCodeObject, let qrCode: String = qrCodeInfo.stringValue else { return } self.stopCapture() // Vibrate AudioServicesPlaySystemSound(kSystemSoundID_Vibrate) self.scanDelegate?.controller(self, didDetectQRCodeWith: qrCode) { [weak self] in self?.startCapture() } } }