mirror of https://github.com/oxen-io/session-ios
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
151 lines
4.9 KiB
Swift
151 lines
4.9 KiB
Swift
3 years ago
|
// 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)
|
||
|
}
|
||
|
}
|