ScanQRViewController.swift 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. //
  2. // ScanQRViewController.swift
  3. // ScanQRCodeLikeWeChat
  4. //
  5. // Created by FancyLou on 2020/8/25.
  6. // Copyright © 2020 muliba. All rights reserved.
  7. //
  8. import UIKit
  9. import AVFoundation
  10. import Photos
  11. typealias ScanResultBlock = (String) -> Void
  12. ///仿微信 扫码功能 全屏版本
  13. class ScanQRViewController: UIViewController {
  14. // MARK: - 返回结果
  15. var resultBlock: ScanResultBlock?
  16. // MARK: - private 参数
  17. ///摄像头输出
  18. private var output: AVCaptureMetadataOutput?
  19. private var session: AVCaptureSession?
  20. private var videoDataOutput: AVCaptureVideoDataOutput?
  21. private var videoPreviewLayer: AVCaptureVideoPreviewLayer?
  22. ///识别结果绘制图集
  23. private var reconizationViews: [ScanRecoObj] = []
  24. ///定时器
  25. private var animationTimeInterval: TimeInterval = 0.02
  26. private var timer: Timer?
  27. private var hasEntered = false
  28. private var scanBorderW: CGFloat = 0
  29. private var scanBorderX: CGFloat = 0
  30. private var scanBorderY: CGFloat = 0
  31. // MARK: - UI
  32. private var backBtn: UIButton!
  33. private var scanningline: UIImageView?
  34. // MARK: - system lifecycle func
  35. override func viewDidAppear(_ animated: Bool) {
  36. super.viewDidAppear(animated)
  37. self.addTimer()
  38. }
  39. override func viewWillDisappear(_ animated: Bool) {
  40. super.viewWillDisappear(animated)
  41. self.removeTimer()
  42. }
  43. override func viewDidLoad() {
  44. super.viewDidLoad()
  45. self.view.backgroundColor = UIColor.black
  46. self.scanBorderW = 0.9 * self.view.frame.size.width
  47. self.scanBorderX = 0.5 * (1 - 0.9) * self.view.frame.size.width
  48. self.scanBorderY = 0.25 * self.view.frame.size.height
  49. self.initUI()
  50. self.prepareScanQRCodeWithResult { (_) in
  51. self.scanSessionStart()
  52. }
  53. }
  54. // MARK: - private func
  55. private func initUI() {
  56. //返回按钮
  57. self.backBtn = UIButton(type: .custom)
  58. self.backBtn.frame = CGRect(x: 15, y: 44, width: 44, height: 44)
  59. self.backBtn.setImage(UIImage(named: "scan_back"), for: .normal)
  60. self.backBtn.addTarget(self, action: #selector(closeSelf), for: .touchUpInside)
  61. self.view.addSubview(self.backBtn)
  62. //选择相册按钮
  63. let photosBtn = UIButton(type: .custom)
  64. photosBtn.frame = CGRect(x: (self.view.bounds.size.width - 44) / 2, y: self.view.bounds.size.height - 44 - 34 - 40, width: 44, height: 44)
  65. photosBtn.setImage(UIImage(named: "photos_icon"), for: .normal)
  66. photosBtn.addTarget(self, action: #selector(photosAction), for: .touchUpInside)
  67. self.view.addSubview(photosBtn)
  68. }
  69. ///开始扫描
  70. private func prepareScanQRCodeWithResult(block: @escaping (String) -> Void) {
  71. //判断设备权限
  72. guard let _ = AVCaptureDevice.default(for: .video) else {
  73. print("没有检测到摄像头,请在真机上测试!")
  74. return
  75. }
  76. let authStatus = AVCaptureDevice.authorizationStatus(for: .video)
  77. if authStatus == .restricted {
  78. let alertC = UIAlertController(title: "提示", message: "无法访问相机,请检查权限", preferredStyle: .alert)
  79. let okAction = UIAlertAction(title: "确定", style: .default, handler: { action in
  80. })
  81. alertC.addAction(okAction)
  82. DispatchQueue.main.async {
  83. self.present(alertC, animated: true, completion: nil)
  84. }
  85. } else if authStatus == .denied { //拒绝相机访问权限
  86. var name: String? = ""
  87. if let dic = Bundle.main.infoDictionary {
  88. name = dic["CFBundleDisplayName"] as? String
  89. if name == nil {
  90. name = dic["CFBundleName"] as? String
  91. }
  92. }
  93. let message = "[前往:设置 - 隐私 - 相机 - \(name ?? "")] 允许应用访问"
  94. let alertC = UIAlertController(title: "提示", message: message, preferredStyle: .alert)
  95. let okAction = UIAlertAction(title: "确定", style: .default, handler: { action in
  96. })
  97. alertC.addAction(okAction)
  98. DispatchQueue.main.async {
  99. self.present(alertC, animated: true, completion: nil)
  100. }
  101. } else if authStatus == .authorized { //已经允许
  102. DispatchQueue.main.async { block("") }
  103. } else if authStatus == .notDetermined { //初始未选择
  104. AVCaptureDevice.requestAccess(for: .video) { (granted) in
  105. if granted {
  106. DispatchQueue.main.async { block("") }
  107. } else {
  108. //todo 拒绝访问。。。。
  109. }
  110. }
  111. }
  112. }
  113. ///准备摄像头,正式开始扫描
  114. private func scanSessionStart() {
  115. // 获取摄像头
  116. guard let device = AVCaptureDevice.default(for: .video) else {
  117. print("没有检测到摄像头,请在真机上测试!")
  118. return
  119. }
  120. var input: AVCaptureDeviceInput?
  121. do {
  122. input = try AVCaptureDeviceInput(device: device)
  123. } catch {
  124. print("err: \(error.localizedDescription)")
  125. }
  126. guard let deviceInput = input else {
  127. print("输入流创建失败")
  128. return
  129. }
  130. //输出流
  131. self.output = AVCaptureMetadataOutput()
  132. self.output?.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
  133. // 注:微信二维码的扫描范围是整个屏幕,这里并没有做处理(可不用设置);
  134. // 如需限制扫描框范围,如下
  135. // if !cropRect.equalTo(CGRect.zero)
  136. // {
  137. //启动相机后,直接修改该参数无效
  138. // output.rectOfInterest = cropRect
  139. // }
  140. //创建session
  141. self.session = AVCaptureSession()
  142. self.session?.sessionPreset = .high
  143. //添加元数据输出流到会话对象
  144. self.session?.addOutput(self.output!)
  145. //创建摄像数据输出流并将其添加到会话对象上, --> 用于识别光线强弱
  146. self.videoDataOutput = AVCaptureVideoDataOutput()
  147. self.videoDataOutput?.setSampleBufferDelegate(self, queue: DispatchQueue.main)
  148. self.session?.addOutput(self.videoDataOutput!)
  149. //添加摄像设备输入流到会话对象
  150. self.session?.addInput(deviceInput)
  151. //识别类型
  152. self.output?.metadataObjectTypes = [.qr, .ean13, .ean8, .code128]
  153. self.videoPreviewLayer = AVCaptureVideoPreviewLayer(session: self.session!)
  154. self.videoPreviewLayer?.videoGravity = .resizeAspectFill
  155. self.videoPreviewLayer?.frame = self.view.bounds
  156. self.view.layer.insertSublayer(self.videoPreviewLayer!, at: 0)
  157. self.session?.startRunning()
  158. }
  159. ///添加定时器
  160. private func addTimer() {
  161. if self.session != nil && self.session?.isRunning != true && hasEntered {
  162. self.session?.startRunning()
  163. }
  164. hasEntered = true
  165. //扫描的时候绿色线条
  166. self.scanningline = UIImageView(image: UIImage(named: "QRCodeScanLine"))
  167. self.view.addSubview(self.scanningline!)
  168. self.scanningline?.frame = CGRect(x: self.scanBorderX, y: self.scanBorderY, width: self.scanBorderW, height: 12)
  169. self.scanningline?.isHidden = true
  170. self.timer = Timer.scheduledTimer(timeInterval: self.animationTimeInterval, target: self, selector: #selector(beginRefreshUI), userInfo: nil, repeats: true)
  171. self.timer?.fire()
  172. }
  173. ///移除定时器
  174. private func removeTimer() {
  175. self.timer?.invalidate()
  176. self.timer = nil
  177. self.scanningline?.removeFromSuperview()
  178. self.scanningline = nil
  179. if self.session?.isRunning == true {
  180. self.session?.stopRunning()
  181. }
  182. }
  183. //开始动画
  184. @objc private func beginRefreshUI() {
  185. //防止还没开始执行定时器就扫描到码,导致扫描动画一直进行
  186. if self.session?.isRunning != true {
  187. self.removeTimer()
  188. return
  189. }
  190. self.scanningline?.isHidden = false
  191. var frame = self.scanningline?.frame
  192. if self.scanningline?.frame.origin.y ?? self.scanBorderY >= self.scanBorderY {
  193. let maxY = self.view.frame.size.height - self.scanBorderY
  194. if self.scanningline?.frame.origin.y ?? self.scanBorderY >= maxY - 10 {
  195. frame?.origin.y = self.scanBorderY
  196. self.scanningline?.frame = frame!
  197. } else {
  198. UIView.animate(withDuration: self.animationTimeInterval) {
  199. frame?.origin.y += 2
  200. self.scanningline?.frame = frame!
  201. }
  202. }
  203. }
  204. }
  205. ///返回结果 关闭页面
  206. private func processWithResult(result: String) {
  207. self.resultBlock?(result)
  208. self.dismiss(animated: true, completion: nil)
  209. }
  210. ///关闭当前扫描页面
  211. @objc private func closeSelf() {
  212. if self.session?.isRunning == true {
  213. self.session?.stopRunning()
  214. }
  215. self.dismiss(animated: true, completion: nil)
  216. }
  217. //取消扫描出来的结果 继续扫描
  218. @objc private func cancelResult() {
  219. if self.session?.isRunning == true {
  220. self.session?.stopRunning()
  221. }
  222. self.reconizationViews.forEach { (obj) in
  223. obj.codeView.removeFromSuperview()
  224. }
  225. self.reconizationViews.removeAll()
  226. if self.session?.isRunning == false {
  227. self.addTimer()
  228. }
  229. self.backBtn.isHidden = false
  230. }
  231. ///从相册选择照片
  232. @objc private func photosAction() {
  233. let authStatus = PHPhotoLibrary.authorizationStatus()
  234. if authStatus == .restricted {
  235. let alertC = UIAlertController(title: "提示", message: "无法访问相册,请检查权限", preferredStyle: .alert)
  236. let okAction = UIAlertAction(title: "确定", style: .default, handler: { action in
  237. })
  238. alertC.addAction(okAction)
  239. DispatchQueue.main.async {
  240. self.present(alertC, animated: true, completion: nil)
  241. }
  242. } else if authStatus == .denied { //拒绝相机访问权限
  243. var name: String? = ""
  244. if let dic = Bundle.main.infoDictionary {
  245. name = dic["CFBundleDisplayName"] as? String
  246. if name == nil {
  247. name = dic["CFBundleName"] as? String
  248. }
  249. }
  250. let message = "[前往:设置 - 隐私 - 照片 - \(name ?? "")] 允许应用访问"
  251. let alertC = UIAlertController(title: "提示", message: message, preferredStyle: .alert)
  252. let okAction = UIAlertAction(title: "确定", style: .default, handler: { action in
  253. })
  254. alertC.addAction(okAction)
  255. DispatchQueue.main.async {
  256. self.present(alertC, animated: true, completion: nil)
  257. }
  258. } else if authStatus == .authorized { //已经允许
  259. DispatchQueue.main.async { self.enterPhotos() }
  260. } else if authStatus == .notDetermined { //初始未选择
  261. PHPhotoLibrary.requestAuthorization { (status) in
  262. if status == .authorized {
  263. DispatchQueue.main.async { self.enterPhotos() }
  264. } else {
  265. //todo 拒绝访问。。。。
  266. }
  267. }
  268. }
  269. }
  270. @objc private func clickCurrentCode(btn: UIButton) {
  271. let obj = self.reconizationViews[btn.tag]
  272. self.processWithResult(result: obj.codeString)
  273. }
  274. ///打开相册
  275. private func enterPhotos() {
  276. let imagePicker = UIImagePickerController()
  277. imagePicker.sourceType = .photoLibrary
  278. imagePicker.delegate = self
  279. imagePicker.modalPresentationStyle = .fullScreen
  280. self.present(imagePicker, animated: true, completion: nil)
  281. }
  282. ///生成遮障层
  283. private func getMaskView(showTips: Bool) -> UIView {
  284. let maskView = UIView(frame: self.view.bounds)
  285. maskView.backgroundColor = UIColor(displayP3Red: 0, green: 0, blue: 0, alpha: 0.6)
  286. if showTips {
  287. let cancel = UIButton(type: .custom)
  288. cancel.frame = CGRect(x: 15, y: 44, width: 50, height: 44)
  289. cancel.setTitle("取消", for: .normal)
  290. cancel.setTitleColor(.white, for: .normal)
  291. cancel.addTarget(self, action: #selector(cancelResult), for: .touchUpInside)
  292. maskView.addSubview(cancel)
  293. let tips = UILabel(frame: CGRect(x: 20, y: self.view.bounds.size.height - 64 - 50, width: self.view.bounds.size.width - 40, height: 50))
  294. tips.text = "轻触小蓝点,选中识别二维码"
  295. tips.font = UIFont.boldSystemFont(ofSize: 14)
  296. tips.textAlignment = .center
  297. tips.textColor = UIColor(displayP3Red: 1, green: 1, blue: 1, alpha: 0.6)
  298. maskView.addSubview(tips)
  299. }
  300. return maskView
  301. }
  302. //动画
  303. private func getAnimation() -> CAKeyframeAnimation {
  304. let ani = CAKeyframeAnimation(keyPath: "transform.scale")
  305. ani.duration = 2.8
  306. ani.isRemovedOnCompletion = false
  307. ani.repeatCount = HUGE
  308. ani.fillMode = .forwards
  309. ani.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
  310. let v1 = NSNumber(value: 1.0)
  311. let v2 = NSNumber(value: 0.8)
  312. ani.values = [v1, v2, v1, v2, v1, v1, v1, v1]
  313. return ani
  314. }
  315. ///生成扫码结果提示按钮
  316. private func getScanResultCode(bounds: CGRect, icon: Bool) -> UIButton {
  317. let btn = UIButton(type: .custom)
  318. btn.frame = bounds
  319. btn.backgroundColor = UIColor(displayP3Red: 54 / 255.0, green: 85 / 255.0, blue: 230 / 255.0, alpha: 1)
  320. btn.addTarget(self, action: #selector(clickCurrentCode(btn:)), for: .touchUpInside)
  321. if icon {
  322. btn.setImage(UIImage(named: "right_arrow_icon"), for: .normal)
  323. btn.layer.add(self.getAnimation(), forKey: "scale-layer")
  324. }
  325. var rect = btn.frame
  326. let center = btn.center
  327. rect.size.width = 40
  328. rect.size.height = 40
  329. btn.frame = rect
  330. btn.center = center
  331. btn.layer.cornerRadius = 20
  332. btn.clipsToBounds = true
  333. btn.layer.borderColor = UIColor.white.cgColor
  334. btn.layer.borderWidth = 3
  335. return btn
  336. }
  337. }
  338. // MARK: - 摄像头识别代理
  339. extension ScanQRViewController: AVCaptureMetadataOutputObjectsDelegate, AVCaptureVideoDataOutputSampleBufferDelegate {
  340. func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
  341. if metadataObjects.count > 0 {
  342. self.removeTimer()
  343. if #available(iOS 10, *) {
  344. let impact = UIImpactFeedbackGenerator(style: .light)
  345. impact.impactOccurred()
  346. }
  347. let maskView = self.getMaskView(showTips: metadataObjects.count > 1)
  348. maskView.alpha = 0
  349. self.view.addSubview(maskView)
  350. UIView.animate(withDuration: 0.6) {
  351. maskView.alpha = 1
  352. }
  353. let obj = ScanRecoObj(codeView: maskView, codeString: "")
  354. self.reconizationViews.append(obj)
  355. var indx = 0
  356. for meta in metadataObjects {
  357. if meta.isKind(of: AVMetadataMachineReadableCodeObject.self) {
  358. let code = self.videoPreviewLayer?.transformedMetadataObject(for: meta) as! AVMetadataMachineReadableCodeObject
  359. let codeBtn = self.getScanResultCode(bounds: code.bounds, icon: metadataObjects.count > 1)
  360. codeBtn.tag = indx + 1
  361. self.view.addSubview(codeBtn)
  362. let codeObj = ScanRecoObj(codeView: codeBtn, codeString: code.stringValue ?? "")
  363. self.reconizationViews.append(codeObj)
  364. }
  365. indx += 1
  366. }
  367. self.backBtn.isHidden = true
  368. if metadataObjects.count == 1 { //只有一个直接返回结果 不需要点击
  369. DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.8) {
  370. self.processWithResult(result: self.reconizationViews[1].codeString)
  371. }
  372. }
  373. }
  374. }
  375. }
  376. // MARK: - 相册选择器代理
  377. extension ScanQRViewController: UINavigationControllerDelegate, UIImagePickerControllerDelegate {
  378. func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
  379. self.dismiss(animated: true, completion: nil)
  380. }
  381. func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
  382. guard let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage else {
  383. return
  384. }
  385. let context = CIContext(options: nil)
  386. let detector = CIDetector(ofType: CIDetectorTypeQRCode, context: context, options: [CIDetectorAccuracy: CIDetectorAccuracyHigh])
  387. guard let ciImage = CIImage(image: image) else {
  388. return
  389. }
  390. let features = detector?.features(in: ciImage)
  391. if features?.count == 0 {
  392. DispatchQueue.main.async {
  393. self.dismiss(animated: true) {
  394. self.processWithResult(result: "")
  395. }
  396. }
  397. } else {
  398. if let feature = features?.first as? CIQRCodeFeature, let result = feature.messageString {
  399. DispatchQueue.main.async {
  400. self.dismiss(animated: true) {
  401. self.processWithResult(result: result)
  402. }
  403. }
  404. }
  405. }
  406. }
  407. }