QrCodeReaderController.swift 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. import AVFoundation
  2. import UIKit
  3. import DcCore
  4. class QrCodeReaderController: UIViewController {
  5. weak var delegate: QrCodeReaderDelegate?
  6. private let captureSession = AVCaptureSession()
  7. private var infoLabelBottomConstraint: NSLayoutConstraint?
  8. private var infoLabelCenterConstraint: NSLayoutConstraint?
  9. private lazy var videoPreviewLayer: AVCaptureVideoPreviewLayer = {
  10. let videoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
  11. videoPreviewLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
  12. return videoPreviewLayer
  13. }()
  14. private var infoLabel: UILabel = {
  15. let label = UILabel()
  16. label.translatesAutoresizingMaskIntoConstraints = false
  17. label.text = String.localized("qrscan_hint")
  18. label.lineBreakMode = .byWordWrapping
  19. label.numberOfLines = 0
  20. label.textAlignment = .center
  21. label.textColor = .white
  22. label.adjustsFontForContentSizeCategory = true
  23. label.font = .preferredFont(forTextStyle: .subheadline)
  24. return label
  25. }()
  26. private let supportedCodeTypes = [
  27. AVMetadataObject.ObjectType.qr
  28. ]
  29. // MARK: - lifecycle
  30. override func viewDidLoad() {
  31. super.viewDidLoad()
  32. self.edgesForExtendedLayout = []
  33. title = String.localized("qrscan_title")
  34. self.setupInfoLabel()
  35. if AVCaptureDevice.authorizationStatus(for: .video) == .authorized {
  36. self.setupQRCodeScanner()
  37. } else {
  38. AVCaptureDevice.requestAccess(for: .video, completionHandler: { [weak self] (granted: Bool) in
  39. guard let self = self else { return }
  40. if granted {
  41. self.setupQRCodeScanner()
  42. } else {
  43. self.showCameraWarning()
  44. self.showPermissionAlert()
  45. }
  46. })
  47. }
  48. }
  49. override func viewWillAppear(_ animated: Bool) {
  50. super.viewWillAppear(animated)
  51. startSession()
  52. }
  53. override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
  54. super.viewWillTransition(to: size, with: coordinator)
  55. coordinator.animate(alongsideTransition: nil, completion: { [weak self] _ in
  56. DispatchQueue.main.async(execute: {
  57. self?.updateVideoOrientation()
  58. })
  59. })
  60. }
  61. override func viewWillDisappear(_ animated: Bool) {
  62. stopSession()
  63. }
  64. // MARK: - setup
  65. private func setupQRCodeScanner() {
  66. guard let captureDevice = AVCaptureDevice.DiscoverySession.init(
  67. deviceTypes: [AVCaptureDevice.DeviceType.builtInWideAngleCamera],
  68. mediaType: .video,
  69. position: .back).devices.first else {
  70. self.showCameraWarning()
  71. return
  72. }
  73. do {
  74. let input = try AVCaptureDeviceInput(device: captureDevice)
  75. self.captureSession.addInput(input)
  76. let captureMetadataOutput = AVCaptureMetadataOutput()
  77. self.captureSession.addOutput(captureMetadataOutput)
  78. captureMetadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
  79. captureMetadataOutput.metadataObjectTypes = self.supportedCodeTypes
  80. } catch {
  81. // If any error occurs, simply print it out and don't continue any more.
  82. self.showCameraWarning()
  83. return
  84. }
  85. view.layer.addSublayer(videoPreviewLayer)
  86. videoPreviewLayer.frame = view.layer.bounds
  87. }
  88. private func setupInfoLabel() {
  89. view.addSubview(infoLabel)
  90. infoLabel.translatesAutoresizingMaskIntoConstraints = false
  91. infoLabelBottomConstraint = infoLabel.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -10)
  92. infoLabelCenterConstraint = infoLabel.constraintCenterYTo(view)
  93. infoLabelBottomConstraint?.isActive = true
  94. infoLabel.constraintAlignLeadingTo(view, paddingLeading: 5).isActive = true
  95. infoLabel.constraintAlignTrailingTo(view, paddingTrailing: 5).isActive = true
  96. view.bringSubviewToFront(infoLabel)
  97. }
  98. private func showCameraWarning() {
  99. DispatchQueue.main.async { [weak self] in
  100. guard let self = self else { return }
  101. let text = String.localized("chat_camera_unavailable")
  102. logger.error(text)
  103. self.infoLabel.textColor = DcColors.defaultTextColor
  104. self.infoLabel.text = text
  105. self.infoLabelBottomConstraint?.isActive = false
  106. self.infoLabelCenterConstraint?.isActive = true
  107. }
  108. }
  109. private func showPermissionAlert() {
  110. DispatchQueue.main.async { [weak self] in
  111. let alert = UIAlertController(title: String.localized("perm_required_title"),
  112. message: String.localized("perm_ios_explain_access_to_camera_denied"),
  113. preferredStyle: .alert)
  114. if let appSettings = URL(string: UIApplication.openSettingsURLString) {
  115. alert.addAction(UIAlertAction(title: String.localized("open_settings"), style: .default, handler: { _ in
  116. UIApplication.shared.open(appSettings, options: [:], completionHandler: nil)}))
  117. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .destructive, handler: nil))
  118. }
  119. self?.present(alert, animated: true, completion: nil)
  120. }
  121. }
  122. private func updateVideoOrientation() {
  123. guard let connection = videoPreviewLayer.connection else {
  124. return
  125. }
  126. guard connection.isVideoOrientationSupported else {
  127. return
  128. }
  129. let statusBarOrientation = UIApplication.shared.statusBarOrientation
  130. let videoOrientation: AVCaptureVideoOrientation = statusBarOrientation.videoOrientation ?? .portrait
  131. if connection.videoOrientation == videoOrientation {
  132. print("no change to videoOrientation")
  133. return
  134. }
  135. videoPreviewLayer.frame = view.bounds
  136. connection.videoOrientation = videoOrientation
  137. videoPreviewLayer.removeAllAnimations()
  138. }
  139. // MARK: - actions
  140. func startSession() {
  141. #if targetEnvironment(simulator)
  142. // ignore if run from simulator
  143. #else
  144. captureSession.startRunning()
  145. #endif
  146. }
  147. func stopSession() {
  148. captureSession.stopRunning()
  149. }
  150. }
  151. extension QrCodeReaderController: AVCaptureMetadataOutputObjectsDelegate {
  152. func metadataOutput(_: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from _: AVCaptureConnection) {
  153. if let metadataObj = metadataObjects[0] as? AVMetadataMachineReadableCodeObject {
  154. if supportedCodeTypes.contains(metadataObj.type) {
  155. if metadataObj.stringValue != nil {
  156. self.captureSession.stopRunning()
  157. self.delegate?.handleQrCode(metadataObj.stringValue!)
  158. }
  159. }
  160. }
  161. }
  162. }
  163. extension UIInterfaceOrientation {
  164. var videoOrientation: AVCaptureVideoOrientation? {
  165. switch self {
  166. case .portraitUpsideDown: return .portraitUpsideDown
  167. case .landscapeRight: return .landscapeRight
  168. case .landscapeLeft: return .landscapeLeft
  169. case .portrait: return .portrait
  170. default: return nil
  171. }
  172. }
  173. }