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 }