lnlink

iOS app for connecting to lightning nodes
git clone git://jb55.com/lnlink
Log | Files | Refs | Submodules | README | LICENSE

ScannerViewController.swift (11340B)


      1 //
      2 //  CodeScanner.swift
      3 //  https://github.com/twostraws/CodeScanner
      4 //
      5 //  Created by Paul Hudson on 14/12/2021.
      6 //  Copyright © 2021 Paul Hudson. All rights reserved.
      7 //
      8 
      9 import AVFoundation
     10 import UIKit
     11 
     12 extension CodeScannerView {
     13     
     14     @available(macCatalyst 14.0, *)
     15     public class ScannerViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
     16         
     17         var delegate: ScannerCoordinator?
     18         private let showViewfinder: Bool
     19         
     20         private var isGalleryShowing: Bool = false {
     21             didSet {
     22                 // Update binding
     23                 if delegate?.parent.isGalleryPresented.wrappedValue != isGalleryShowing {
     24                     delegate?.parent.isGalleryPresented.wrappedValue = isGalleryShowing
     25                 }
     26             }
     27         }
     28 
     29         public init(showViewfinder: Bool = false) {
     30             self.showViewfinder = showViewfinder
     31             super.init(nibName: nil, bundle: nil)
     32         }
     33 
     34         required init?(coder: NSCoder) {
     35             self.showViewfinder = false
     36             super.init(coder: coder)
     37         }
     38         
     39         func openGallery() {
     40             isGalleryShowing = true
     41             let imagePicker = UIImagePickerController()
     42             imagePicker.delegate = self
     43             present(imagePicker, animated: true, completion: nil)
     44         }
     45         
     46         @objc func openGalleryFromButton(_ sender: UIButton) {
     47             openGallery()
     48         }
     49 
     50         public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
     51             isGalleryShowing = false
     52             
     53             if let qrcodeImg = info[.originalImage] as? UIImage {
     54                 let detector = CIDetector(ofType: CIDetectorTypeQRCode, context: nil, options: [CIDetectorAccuracy: CIDetectorAccuracyHigh])!
     55                 let ciImage = CIImage(image:qrcodeImg)!
     56                 var qrCodeLink = ""
     57 
     58                 let features = detector.features(in: ciImage)
     59 
     60                 for feature in features as! [CIQRCodeFeature] {
     61                     qrCodeLink += feature.messageString!
     62                 }
     63 
     64                 if qrCodeLink == "" {
     65                     delegate?.didFail(reason: .badOutput)
     66                 } else {
     67                     let result = ScanResult(string: qrCodeLink, type: .qr)
     68                     delegate?.found(result)
     69                 }
     70             } else {
     71                 print("Something went wrong")
     72             }
     73 
     74             dismiss(animated: true, completion: nil)
     75         }
     76         
     77         public func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
     78             isGalleryShowing = false
     79         }
     80 
     81         #if targetEnvironment(simulator)
     82         override public func loadView() {
     83             view = UIView()
     84             view.isUserInteractionEnabled = true
     85 
     86             let label = UILabel()
     87             label.translatesAutoresizingMaskIntoConstraints = false
     88             label.numberOfLines = 0
     89             label.text = "You're running in the simulator, which means the camera isn't available. Tap anywhere to send back some simulated data."
     90             label.textAlignment = .center
     91 
     92             let button = UIButton()
     93             button.translatesAutoresizingMaskIntoConstraints = false
     94             button.setTitle("Select a custom image", for: .normal)
     95             button.setTitleColor(UIColor.systemBlue, for: .normal)
     96             button.setTitleColor(UIColor.gray, for: .highlighted)
     97             button.addTarget(self, action: #selector(openGalleryFromButton), for: .touchUpInside)
     98 
     99             let stackView = UIStackView()
    100             stackView.translatesAutoresizingMaskIntoConstraints = false
    101             stackView.axis = .vertical
    102             stackView.spacing = 50
    103             stackView.addArrangedSubview(label)
    104             stackView.addArrangedSubview(button)
    105 
    106             view.addSubview(stackView)
    107 
    108             NSLayoutConstraint.activate([
    109                 button.heightAnchor.constraint(equalToConstant: 50),
    110                 stackView.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor),
    111                 stackView.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor),
    112                 stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
    113             ])
    114         }
    115 
    116         override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    117             guard let simulatedData = delegate?.parent.simulatedData else {
    118                 print("Simulated Data Not Provided!")
    119                 return
    120             }
    121 
    122             // Send back their simulated data, as if it was one of the types they were scanning for
    123             let result = ScanResult(string: simulatedData, type: delegate?.parent.codeTypes.first ?? .qr)
    124             delegate?.found(result)
    125         }
    126         
    127         #else
    128         
    129         var captureSession: AVCaptureSession!
    130         var previewLayer: AVCaptureVideoPreviewLayer!
    131         let fallbackVideoCaptureDevice = AVCaptureDevice.default(for: .video)
    132 
    133         private lazy var viewFinder: UIImageView? = {
    134             guard let image = UIImage(named: "viewfinder", in: .main, with: nil) else {
    135                 return nil
    136             }
    137 
    138             let imageView = UIImageView(image: image)
    139             imageView.translatesAutoresizingMaskIntoConstraints = false
    140             return imageView
    141         }()
    142 
    143         override public func viewDidLoad() {
    144             super.viewDidLoad()
    145 
    146             NotificationCenter.default.addObserver(self,
    147                                                    selector: #selector(updateOrientation),
    148                                                    name: Notification.Name("UIDeviceOrientationDidChangeNotification"),
    149                                                    object: nil)
    150 
    151             view.backgroundColor = UIColor.black
    152             captureSession = AVCaptureSession()
    153 
    154             guard let videoCaptureDevice = delegate?.parent.videoCaptureDevice ?? fallbackVideoCaptureDevice else {
    155                 return
    156             }
    157 
    158             let videoInput: AVCaptureDeviceInput
    159 
    160             do {
    161                 videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice)
    162             } catch {
    163                 delegate?.didFail(reason: .initError(error))
    164                 return
    165             }
    166 
    167             if (captureSession.canAddInput(videoInput)) {
    168                 captureSession.addInput(videoInput)
    169             } else {
    170                 delegate?.didFail(reason: .badInput)
    171                 return
    172             }
    173 
    174             let metadataOutput = AVCaptureMetadataOutput()
    175 
    176             if (captureSession.canAddOutput(metadataOutput)) {
    177                 captureSession.addOutput(metadataOutput)
    178 
    179                 metadataOutput.setMetadataObjectsDelegate(delegate, queue: DispatchQueue.main)
    180                 metadataOutput.metadataObjectTypes = delegate?.parent.codeTypes
    181             } else {
    182                 delegate?.didFail(reason: .badOutput)
    183                 return
    184             }
    185         }
    186 
    187         override public func viewWillLayoutSubviews() {
    188             previewLayer?.frame = view.layer.bounds
    189         }
    190 
    191         @objc func updateOrientation() {
    192             guard let orientation = view.window?.windowScene?.interfaceOrientation else { return }
    193             guard let connection = captureSession.connections.last, connection.isVideoOrientationSupported else { return }
    194             connection.videoOrientation = AVCaptureVideoOrientation(rawValue: orientation.rawValue) ?? .portrait
    195         }
    196 
    197         override public func viewDidAppear(_ animated: Bool) {
    198             super.viewDidAppear(animated)
    199             updateOrientation()
    200         }
    201 
    202         override public func viewWillAppear(_ animated: Bool) {
    203             super.viewWillAppear(animated)
    204 
    205             if previewLayer == nil {
    206                 previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
    207             }
    208 
    209             previewLayer.frame = view.layer.bounds
    210             previewLayer.videoGravity = .resizeAspectFill
    211             view.layer.addSublayer(previewLayer)
    212             addviewfinder()
    213 
    214             delegate?.reset()
    215 
    216             if (captureSession?.isRunning == false) {
    217                 DispatchQueue.global(qos: .userInitiated).async {
    218                     self.captureSession.startRunning()
    219                 }
    220             }
    221         }
    222 
    223         private func addviewfinder() {
    224             guard showViewfinder, let imageView = viewFinder else { return }
    225 
    226             view.addSubview(imageView)
    227 
    228             NSLayoutConstraint.activate([
    229                 imageView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
    230                 imageView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
    231                 imageView.widthAnchor.constraint(equalToConstant: 200),
    232                 imageView.heightAnchor.constraint(equalToConstant: 200),
    233             ])
    234         }
    235 
    236         override public func viewDidDisappear(_ animated: Bool) {
    237             super.viewDidDisappear(animated)
    238 
    239             if (captureSession?.isRunning == true) {
    240                 DispatchQueue.global(qos: .userInitiated).async {
    241                     self.captureSession.stopRunning()
    242                 }
    243             }
    244 
    245             NotificationCenter.default.removeObserver(self)
    246         }
    247 
    248         override public var prefersStatusBarHidden: Bool {
    249             true
    250         }
    251 
    252         override public var supportedInterfaceOrientations: UIInterfaceOrientationMask {
    253             .all
    254         }
    255 
    256         /** Touch the screen for autofocus */
    257         public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    258             guard touches.first?.view == view,
    259                   let touchPoint = touches.first,
    260                   let device = delegate?.parent.videoCaptureDevice ?? fallbackVideoCaptureDevice
    261             else { return }
    262 
    263             let videoView = view
    264             let screenSize = videoView!.bounds.size
    265             let xPoint = touchPoint.location(in: videoView).y / screenSize.height
    266             let yPoint = 1.0 - touchPoint.location(in: videoView).x / screenSize.width
    267             let focusPoint = CGPoint(x: xPoint, y: yPoint)
    268 
    269             do {
    270                 try device.lockForConfiguration()
    271             } catch {
    272                 return
    273             }
    274 
    275             // Focus to the correct point, make continiuous focus and exposure so the point stays sharp when moving the device closer
    276             device.focusPointOfInterest = focusPoint
    277             device.focusMode = .continuousAutoFocus
    278             device.exposurePointOfInterest = focusPoint
    279             device.exposureMode = AVCaptureDevice.ExposureMode.continuousAutoExposure
    280             device.unlockForConfiguration()
    281         }
    282         
    283         #endif
    284         
    285         func updateViewController(isTorchOn: Bool, isGalleryPresented: Bool) {
    286             if let backCamera = AVCaptureDevice.default(for: AVMediaType.video),
    287                backCamera.hasTorch
    288             {
    289                 try? backCamera.lockForConfiguration()
    290                 backCamera.torchMode = isTorchOn ? .on : .off
    291                 backCamera.unlockForConfiguration()
    292             }
    293             
    294             if isGalleryPresented && !isGalleryShowing {
    295                 openGallery()
    296             }
    297         }
    298         
    299     }
    300 }