ScannerViewController.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  1. //
  2. // ScannerViewController.swift
  3. // scanner
  4. //
  5. // Created by Alva on 2020/5/25.
  6. // Copyright © 2020 Alva. All rights reserved.
  7. //
  8. import SwiftUI
  9. import AVFoundation
  10. var SCREENWidth = UIScreen.main.bounds.size.width
  11. var SCREENHeight = UIScreen.main.bounds.size.height
  12. let QRCodeWidth = Double(min(SCREENWidth, SCREENHeight)) / 1.5
  13. let RATIO = 0.45
  14. enum ArgumentsEnum: String {
  15. case title = "SCAN_TITLE"
  16. case laserColor = "LASER_COLOR"
  17. case titleColor = "TITLE_COLOR"
  18. case playBeep = "KEY_PLAY_BEEP"
  19. case scanWidth = "SCAN_WIDTH"
  20. case scanHeight = "SCAN_HEIGHT"
  21. case promptMessage = "PROMPT_MESSAGE"
  22. case permissionDeniedMessage = "PERMISSION_DENIED_MESSAGE"
  23. case confirmText = "MESSAGE_CONFIRM_TEXT"
  24. case cancelText = "MESSAGE_CANCEL_TEXT"
  25. func getKeyValue<T>(dictionary: NSDictionary) -> T? {
  26. guard let result = dictionary[self.rawValue] as? T else {
  27. return nil
  28. }
  29. return result
  30. }
  31. }
  32. protocol ScannerDelegate: class {
  33. func didScanWithResult(code: String)
  34. func didFailWithErrorCode(code: String)
  35. }
  36. class ScannerViewController: UIViewController, AVCaptureMetadataOutputObjectsDelegate {
  37. @IBOutlet private var camera: UIView!
  38. private var session: AVCaptureSession? = nil
  39. private var top = 0.0
  40. var bundle: Bundle? = nil
  41. var currentOrientation = UIInterfaceOrientationMask.portrait
  42. weak var delegate: ScannerDelegate?
  43. var arguments: NSDictionary = [:]
  44. var laserColor: UIColor = UIColor.clear
  45. var promptMessage: String?
  46. var permissionDeniedText: String = "Your privacy settings seem to prevent us from accessing your camera for barcode scanning. You can fix it by doing this, touch the OK button below to open the Settings and then turn the Camera on."
  47. var confirmText: String = "OK"
  48. var cancelText: String = "Cancel"
  49. var windowOrientation: UIInterfaceOrientation {
  50. if #available(iOS 13.0, *) {
  51. return view.window?.windowScene?.interfaceOrientation ?? .unknown
  52. } else {
  53. return UIApplication.shared.statusBarOrientation
  54. }
  55. }
  56. required init?(coder aDecoder: NSCoder) {
  57. super.init(coder: aDecoder)
  58. }
  59. init() {
  60. super.init(nibName: nil, bundle: nil)
  61. let mainBundle = Bundle(for: type(of: self))
  62. let url = mainBundle.url(forResource: "FlutterScannerBundle", withExtension: "bundle")
  63. if let url = url {
  64. bundle = Bundle(url: url)
  65. }
  66. if(bundle == nil) {
  67. return
  68. }
  69. getCurrentOrientation()
  70. }
  71. override func viewDidLoad() {
  72. super.viewDidLoad()
  73. initArguments()
  74. setupNavigationBar()
  75. }
  76. override func viewWillAppear(_ animated: Bool) {
  77. super.viewWillAppear(animated)
  78. checkAuthorization()
  79. }
  80. override var shouldAutorotate: Bool {
  81. get {
  82. return false
  83. }
  84. }
  85. override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
  86. get {
  87. return currentOrientation
  88. }
  89. }
  90. override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
  91. session?.stopRunning()
  92. SCREENWidth = UIScreen.main.bounds.size.width
  93. SCREENHeight = UIScreen.main.bounds.size.height
  94. checkAuthorization()
  95. }
  96. func getCurrentOrientation(){
  97. switch UIDevice.current.orientation {
  98. case UIDeviceOrientation.faceDown:
  99. break
  100. case UIDeviceOrientation.unknown:
  101. break
  102. case UIDeviceOrientation.portrait:
  103. break
  104. case UIDeviceOrientation.portraitUpsideDown:
  105. break
  106. case UIDeviceOrientation.faceUp:
  107. break
  108. case UIDeviceOrientation.landscapeLeft:
  109. currentOrientation = UIInterfaceOrientationMask.landscapeRight
  110. break
  111. case UIDeviceOrientation.landscapeRight:
  112. currentOrientation = UIInterfaceOrientationMask.landscapeLeft
  113. break
  114. @unknown default:
  115. break
  116. }
  117. }
  118. func initArguments() {
  119. SCREENWidth = UIScreen.main.bounds.size.width
  120. SCREENHeight = UIScreen.main.bounds.size.height
  121. if let titleHex: String = ArgumentsEnum.title.getKeyValue(dictionary: arguments) {
  122. self.title = titleHex
  123. }
  124. if let laserColorHex: String = ArgumentsEnum.laserColor.getKeyValue(dictionary: arguments) {
  125. laserColor = UIColor(hexString: laserColorHex) ?? UIColor.clear
  126. }
  127. if let promptMessageHex: String = ArgumentsEnum.promptMessage.getKeyValue(dictionary: arguments) {
  128. promptMessage = promptMessageHex
  129. }
  130. if let permissionDeniedTextHex: String = ArgumentsEnum.permissionDeniedMessage.getKeyValue(dictionary: arguments) {
  131. permissionDeniedText = permissionDeniedTextHex
  132. }
  133. if let confirmTextHex: String = ArgumentsEnum.confirmText.getKeyValue(dictionary: arguments) {
  134. confirmText = confirmTextHex
  135. }
  136. if let cancelTextHex: String = ArgumentsEnum.cancelText.getKeyValue(dictionary: arguments) {
  137. cancelText = cancelTextHex
  138. }
  139. }
  140. func checkAuthorization(){
  141. /*
  142. Check the video authorization status. Video access is required and audio
  143. access is optional. If the user denies audio access, AVCam won't
  144. record audio during movie recording.
  145. */
  146. switch AVCaptureDevice.authorizationStatus(for: .video) {
  147. case .authorized:
  148. self.setupMaskView()
  149. self.beginScanning()
  150. break
  151. case .notDetermined:
  152. AVCaptureDevice.requestAccess(for: .video, completionHandler: { granted in
  153. if !granted {
  154. self.permissionDenied()
  155. }
  156. self.setupMaskView()
  157. self.beginScanning()
  158. })
  159. default:
  160. self.permissionDenied()
  161. }
  162. }
  163. func permissionDenied(){
  164. DispatchQueue.main.async {
  165. let alertController = UIAlertController(title: self.title, message: self.permissionDeniedText, preferredStyle: .alert)
  166. let confirmAction = UIAlertAction(title: self.confirmText, style: .default) { (action) in
  167. if let url = URL(string: UIApplication.openSettingsURLString) {
  168. if #available(iOS 10, *) {
  169. UIApplication.shared.open(url, options: [:], completionHandler: {
  170. (success) in
  171. self.dismiss(animated: true, completion: nil)
  172. })
  173. } else {
  174. UIApplication.shared.openURL(url)
  175. self.dismiss(animated: true, completion: nil)
  176. }
  177. }
  178. }
  179. let cancelAction = UIAlertAction(title: self.cancelText, style: .default) { (action) in
  180. self.dismiss(animated: true, completion: nil)
  181. }
  182. alertController.addAction(confirmAction)
  183. alertController.addAction(cancelAction)
  184. self.present(alertController, animated: true)
  185. }
  186. }
  187. func setupMaskView() {
  188. UINib.init(nibName: "FlutterQrScanner", bundle: bundle!).instantiate(withOwner: self, options: nil)
  189. var scanY = Double(SCREENHeight) - top - QRCodeWidth
  190. scanY = scanY * RATIO
  191. let frame = CGRect(x: (Double(SCREENWidth) - QRCodeWidth) / 2.0, y: scanY, width: QRCodeWidth, height: QRCodeWidth)
  192. let backgroundView = UIView(frame: UIScreen.main.bounds)
  193. backgroundView.backgroundColor = UIColor.init(red: 0, green: 0, blue: 0, alpha: 0.6)
  194. camera.addSubview(backgroundView)
  195. let maskLayer = CAShapeLayer()
  196. maskLayer.fillRule = CAShapeLayerFillRule.evenOdd // fill rule
  197. let basicPath = UIBezierPath(rect: UIScreen.main.bounds) // basic
  198. let maskPath = UIBezierPath(roundedRect: frame, cornerRadius: 15)
  199. basicPath.append(maskPath) // recover
  200. maskLayer.path = basicPath.cgPath
  201. backgroundView.layer.mask = maskLayer
  202. let scanBorder = BorderCanvas(frame: frame, border: laserColor)
  203. camera.addSubview(scanBorder)
  204. let scanWindow = UIView(frame: frame)
  205. scanWindow.clipsToBounds = true
  206. camera.addSubview(scanWindow)
  207. let winMaskLayer = CAShapeLayer()
  208. // fill rule
  209. winMaskLayer.fillRule = CAShapeLayerFillRule.evenOdd
  210. let winFrame = CGRect(x: 0, y: 0, width: QRCodeWidth, height: QRCodeWidth)
  211. let winBasicPath = UIBezierPath(rect: winFrame)
  212. let winMaskPath = UIBezierPath(rect: winFrame)
  213. let winMaskPath2 = UIBezierPath(roundedRect: winFrame, cornerRadius: 15)
  214. winBasicPath.append(winMaskPath)
  215. winBasicPath.append(winMaskPath2)
  216. winMaskLayer.path = winBasicPath.cgPath
  217. scanWindow.layer.mask = winMaskLayer
  218. //scan window animation
  219. let scanNetImageViewH = scanWindow.frame.size.height
  220. let scanNetImageViewW = scanWindow.frame.size.width
  221. let scanNetImageView = UIImageView(image: UIImage.init(named: "scannet", in: bundle!, compatibleWith: nil))
  222. scanNetImageView.frame = CGRect(x: 0, y: -scanNetImageViewH, width: scanNetImageViewW, height: scanNetImageViewH)
  223. let scanNetAnimation = CABasicAnimation(keyPath: "transform.translation.y")
  224. scanNetAnimation.byValue = NSNumber(value: QRCodeWidth)
  225. scanNetAnimation.duration = 1.5
  226. scanNetAnimation.repeatCount = MAXFLOAT
  227. scanNetImageView.layer.add(scanNetAnimation, forKey: "animation")
  228. scanWindow.addSubview(scanNetImageView)
  229. let promptSize = CGSize(width: Double(SCREENWidth) - 30, height: 0)
  230. let promptRect = (promptMessage ?? "").boundingRect(with: promptSize, options: NSStringDrawingOptions.usesLineFragmentOrigin, attributes: nil , context: nil)
  231. let promptLabel = UILabel(frame: CGRect(x: 15, y: scanY + QRCodeWidth + 30, width: Double(SCREENWidth) - 30, height: Double(promptRect.size.height)))
  232. promptLabel.textColor = UIColor.gray
  233. promptLabel.text = promptMessage
  234. promptLabel.textAlignment = NSTextAlignment.center
  235. camera.addSubview(promptLabel)
  236. }
  237. func setupNavigationBar(){
  238. let navHeight = self.navigationController?.navigationBar.bounds.height ?? 20
  239. let backButton = UIButton(frame: CGRect(x: 0, y: 0, width: navHeight, height: navHeight))
  240. backButton.setImage(UIImage.init(named: "arrow_left", in: bundle!, compatibleWith: nil), for: .normal)
  241. backButton.setTitle("", for: .normal)
  242. backButton.setTitleColor(backButton.tintColor, for: .normal)
  243. backButton.addTarget(self, action: #selector(backButtonPressed), for: .touchUpInside)
  244. backButton.imageView?.contentMode = .scaleAspectFit
  245. self.navigationItem.leftBarButtonItem = UIBarButtonItem(customView: backButton)
  246. self.navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default)
  247. self.navigationController?.navigationBar.shadowImage = UIImage()
  248. self.navigationController?.navigationBar.isTranslucent = true
  249. self.navigationController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white, NSAttributedString.Key.font: UIFont.systemFont(ofSize:18)]
  250. }
  251. func beginScanning() {
  252. //get device
  253. let device = AVCaptureDevice.default(for: .video)
  254. //create device input
  255. var input: AVCaptureDeviceInput? = nil
  256. do {
  257. if let device = device {
  258. input = try AVCaptureDeviceInput(device: device)
  259. }
  260. } catch {
  261. delegate?.didFailWithErrorCode(code: "")
  262. return
  263. }
  264. if(input == nil){
  265. delegate?.didFailWithErrorCode(code: "")
  266. return
  267. }
  268. //create device output
  269. let output = AVCaptureMetadataOutput()
  270. let xx = (Double(SCREENHeight) - QRCodeWidth - top) * RATIO
  271. let x = xx / Double(SCREENHeight)
  272. let yy = (Double(SCREENWidth) - QRCodeWidth) / 2.0
  273. let y = yy / Double(SCREENWidth)
  274. let width = QRCodeWidth / Double(SCREENHeight)
  275. let height = QRCodeWidth / Double(SCREENWidth)
  276. output.rectOfInterest = CGRect(x: x, y: y, width: width, height: height)
  277. output.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
  278. session = AVCaptureSession()
  279. session!.sessionPreset = .high
  280. if session?.canAddInput(input!) ?? false {
  281. session!.addInput(input!)
  282. session!.addOutput(output)
  283. //code data type
  284. output.metadataObjectTypes = [
  285. .qr,
  286. .ean13,
  287. .ean8,
  288. .code128
  289. ]
  290. let layer = AVCaptureVideoPreviewLayer(session: session!)
  291. layer.frame = CGRect(x: 0, y: 0, width: CGFloat(SCREENWidth), height: SCREENHeight - CGFloat(top))
  292. layer.videoGravity = .resizeAspectFill
  293. camera.layer.insertSublayer(layer, at: 0)
  294. DispatchQueue.main.async {
  295. /*
  296. Dispatch video streaming to the main queue because AVCaptureVideoPreviewLayer is the backing layer for PreviewView.
  297. You can manipulate UIView only on the main thread.
  298. Note: As an exception to the above rule, it's not necessary to serialize video orientation changes
  299. on the AVCaptureVideoPreviewLayer’s connection with other session manipulation.
  300. Use the window scene's orientation as the initial video orientation. Subsequent orientation changes are
  301. handled by CameraViewController.viewWillTransition(to:with:).
  302. */
  303. var initialVideoOrientation: AVCaptureVideoOrientation = .portrait
  304. if self.windowOrientation != .unknown {
  305. if let videoOrientation = AVCaptureVideoOrientation(rawValue: self.windowOrientation.rawValue) {
  306. initialVideoOrientation = videoOrientation
  307. }
  308. }
  309. layer.connection?.videoOrientation = initialVideoOrientation
  310. }
  311. //start
  312. session!.startRunning()
  313. } else {
  314. print("Couldn't add video device input to the session.")
  315. session!.commitConfiguration()
  316. return
  317. }
  318. }
  319. func metadataOutput(_ captureOutput: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
  320. if metadataObjects.count > 0 {
  321. //code result data
  322. let metadataObject = metadataObjects[0] as? AVMetadataMachineReadableCodeObject
  323. delegate?.didScanWithResult(code: metadataObject?.stringValue ?? "")
  324. self.dismiss(animated: true, completion: nil)
  325. }
  326. }
  327. @objc func backButtonPressed() {
  328. delegate?.didFailWithErrorCode(code: "canceled")
  329. self.dismiss(animated: true, completion: nil)
  330. }
  331. override func viewWillDisappear(_ animated: Bool) {
  332. super.viewWillDisappear(animated)
  333. self.session?.stopRunning()
  334. }
  335. }
  336. class BorderCanvas: UIView {
  337. var border: UIColor = UIColor.clear
  338. override init(frame: CGRect) {
  339. super.init(frame: frame)
  340. self.backgroundColor = UIColor.clear
  341. }
  342. convenience init(frame: CGRect, border: UIColor?) {
  343. self.init(frame: frame)
  344. self.border = border ?? UIColor.clear
  345. }
  346. required init?(coder aDecoder: NSCoder) {
  347. fatalError("init(coder:) has not been implemented")
  348. }
  349. override func draw(_ rect: CGRect) {
  350. let pathRect = self.bounds.insetBy(dx: 1, dy: 1)
  351. let path = UIBezierPath(roundedRect: pathRect, cornerRadius: 15)
  352. path.lineWidth = 3
  353. UIColor.clear.setFill()
  354. self.border.setStroke()
  355. path.fill()
  356. path.stroke()
  357. let maskLayer = CAShapeLayer()
  358. maskLayer.fillRule = CAShapeLayerFillRule.evenOdd // fill rule
  359. let basicPath = UIBezierPath(rect: CGRect(x: 0, y: 0, width: QRCodeWidth, height: QRCodeWidth)) // basic
  360. let maskPath = UIBezierPath(rect: CGRect(x: QRCodeWidth / 6, y: 0, width: QRCodeWidth * 2 / 3, height: QRCodeWidth))
  361. let maskPath2 = UIBezierPath(rect: CGRect(x: 0, y: QRCodeWidth / 6, width: QRCodeWidth, height: QRCodeWidth * 2 / 3))
  362. basicPath.append(maskPath) // recover
  363. basicPath.append(maskPath2) // recover
  364. maskLayer.path = basicPath.cgPath
  365. layer.mask = maskLayer
  366. }
  367. }
  368. extension AVCaptureVideoOrientation {
  369. init?(deviceOrientation: UIDeviceOrientation) {
  370. switch deviceOrientation {
  371. case .portrait: self = .portrait
  372. case .portraitUpsideDown: self = .portraitUpsideDown
  373. case .landscapeLeft: self = .landscapeRight
  374. case .landscapeRight: self = .landscapeLeft
  375. default: return nil
  376. }
  377. }
  378. init?(interfaceOrientation: UIInterfaceOrientation) {
  379. switch interfaceOrientation {
  380. case .portrait: self = .portrait
  381. case .portraitUpsideDown: self = .portraitUpsideDown
  382. case .landscapeLeft: self = .landscapeLeft
  383. case .landscapeRight: self = .landscapeRight
  384. default: return nil
  385. }
  386. }
  387. }
  388. extension UIColor {
  389. convenience init(rgba: Int) {
  390. self.init(
  391. red: CGFloat((rgba & 0x00FF0000) >> 16) / 255.0,
  392. green: CGFloat((rgba & 0x0000FF00) >> 8) / 255.0,
  393. blue: CGFloat(rgba & 0x000000FF) / 255.0,
  394. alpha: CGFloat((rgba & 0xFF000000) >> 24) / 255.0
  395. )
  396. }
  397. convenience init?(hexString: String) {
  398. var chars = Array(hexString.hasPrefix("#") ? hexString.dropFirst() : hexString[...])
  399. let red, green, blue, alpha: CGFloat
  400. switch chars.count {
  401. case 3:
  402. chars = chars.flatMap { [$0, $0] }
  403. fallthrough
  404. case 6:
  405. chars = ["F","F"] + chars
  406. fallthrough
  407. case 8:
  408. alpha = CGFloat(strtoul(String(chars[0...1]), nil, 16)) / 255
  409. red = CGFloat(strtoul(String(chars[2...3]), nil, 16)) / 255
  410. green = CGFloat(strtoul(String(chars[4...5]), nil, 16)) / 255
  411. blue = CGFloat(strtoul(String(chars[6...7]), nil, 16)) / 255
  412. default:
  413. return nil
  414. }
  415. self.init(red: red, green: green, blue: blue, alpha: alpha)
  416. }
  417. }