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.
		
		
		
		
		
			
		
			
				
	
	
		
			257 lines
		
	
	
		
			9.6 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			257 lines
		
	
	
		
			9.6 KiB
		
	
	
	
		
			Swift
		
	
| // 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, onSuccess: (() -> ())?, 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
 | |
|     private var shouldResumeCapture: 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 || shouldResumeCapture {
 | |
|             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,
 | |
|             onSuccess: { [weak self] in
 | |
|                 self?.shouldResumeCapture = true
 | |
|             },
 | |
|             onError: { [weak self] in
 | |
|                 self?.startCapture()
 | |
|             }
 | |
|         )
 | |
|     }
 | |
| }
 | |
| 
 | |
| // MARK: SwiftUI
 | |
| import SwiftUI
 | |
| 
 | |
| struct QRCodeScanningVC_SwiftUI: UIViewControllerRepresentable {
 | |
|     typealias UIViewControllerType = QRCodeScanningViewController
 | |
|     
 | |
|     let scanQRCodeVC = QRCodeScanningViewController()
 | |
|     var didDetectQRCode: (String, (() -> ())?, (() -> ())?) -> ()
 | |
|     
 | |
|     func makeUIViewController(context: Context) -> QRCodeScanningViewController {
 | |
|         return scanQRCodeVC
 | |
|     }
 | |
|     
 | |
|     func updateUIViewController(_ scanQRCodeVC: QRCodeScanningViewController, context: Context) {
 | |
|         scanQRCodeVC.startCapture()
 | |
|     }
 | |
|     
 | |
|     func makeCoordinator() -> Coordinator {
 | |
|         Coordinator(
 | |
|             scanQRCodeVC: scanQRCodeVC,
 | |
|             didDetectQRCode: didDetectQRCode
 | |
|         )
 | |
|     }
 | |
|     
 | |
|     class Coordinator: NSObject, QRScannerDelegate {
 | |
|         var didDetectQRCode: (String, (() -> ())?, (() -> ())?) -> ()
 | |
|         
 | |
|         init(
 | |
|             scanQRCodeVC: QRCodeScanningViewController,
 | |
|             didDetectQRCode: @escaping (String, (() -> ())?, (() -> ())?) -> ()
 | |
|         ) {
 | |
|             self.didDetectQRCode = didDetectQRCode
 | |
|             super.init()
 | |
|             scanQRCodeVC.scanDelegate = self
 | |
|         }
 | |
|         
 | |
|         func controller(_ controller: QRCodeScanningViewController, didDetectQRCodeWith string: String, onSuccess: (() -> ())?, onError: (() -> ())?) {
 | |
|             didDetectQRCode(string, onSuccess, onError)
 | |
|         }
 | |
|     }
 | |
| }
 |