session-ios/Session/Shared/QRCodeScanningViewControlle...

151 lines
4.9 KiB
Swift

// 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)
}
}