// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import UIKit import AVFoundation import ZXingObjC import SessionUIKit protocol QRScannerDelegate: AnyObject { func controller(_ controller: QRCodeScanningViewController, didDetectQRCodeWith string: String) } class QRCodeScanningViewController: UIViewController, AVCaptureMetadataOutputObjectsDelegate, ZXCaptureDelegate { public weak var scanDelegate: QRScannerDelegate? private let captureQueue: DispatchQueue = DispatchQueue.global(qos: .default) private var capture: ZXCapture? private var captureEnabled: Bool = false // MARK: - Initialization deinit { self.capture?.layer.removeFromSuperlayer() } // MARK: - Components private let maskingView: UIView = { let result: OWSBezierPathView = OWSBezierPathView() result.configureShapeLayerBlock = { layer, bounds in // Add a circular mask let path: UIBezierPath = UIBezierPath(rect: bounds) let margin: CGFloat = ScaleFromIPhone5To7Plus(24, 48) let radius: CGFloat = ((min(bounds.size.width, bounds.size.height) * 0.5) - margin) // Center the circle's bounding rectangle let circleRect: CGRect = CGRect( x: ((bounds.size.width * 0.5) - radius), y: ((bounds.size.height * 0.5) - radius), width: (radius * 2), height: (radius * 2) ) let circlePath: UIBezierPath = UIBezierPath.init( roundedRect: circleRect, cornerRadius: 16 ) path.append(circlePath) path.usesEvenOddFillRule = true layer.path = path.cgPath layer.fillRule = .evenOdd layer.themeFillColor = .black layer.opacity = 0.32 } return result }() // MARK: - Lifecycle override func loadView() { super.loadView() self.view.addSubview(maskingView) maskingView.pin(to: self.view) } 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() // Note: When accessing 'capture.layer' if the setup hasn't been completed it // will result in a layout being triggered which creates an infinite loop, this // check prevents that case if let capture: ZXCapture = self.capture { capture.layer.frame = self.view.bounds } } // 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 capture: ZXCapture = ZXCapture() capture.camera = capture.back() capture.focusMode = .autoFocus capture.delegate = self capture.start() // Note: When accessing the 'layer' for the first time it will create // an instance of 'AVCaptureVideoPreviewLayer', this can hang a little // so we do this on the background thread first if capture.layer != nil {} DispatchQueue.main.async { capture.layer.frame = (self?.view.bounds ?? .zero) self?.view.layer.addSublayer(capture.layer) if let maskingView: UIView = self?.maskingView { self?.view.bringSubviewToFront(maskingView) } self?.capture = capture } } } else { self.capture?.start() } #endif } private func stopCapture() { self.captureEnabled = false self.captureQueue.async { [weak self] in self?.capture?.stop() } } internal func captureResult(_ capture: ZXCapture, result: ZXResult) { guard self.captureEnabled else { return } self.stopCapture() // Vibrate AudioServicesPlaySystemSound(kSystemSoundID_Vibrate) self.scanDelegate?.controller(self, didDetectQRCodeWith: result.text) } }