123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479 |
- //
- // ScannerViewController.swift
- // scanner
- //
- // Created by Alva on 2020/5/25.
- // Copyright © 2020 Alva. All rights reserved.
- //
- import SwiftUI
- import AVFoundation
- var SCREENWidth = UIScreen.main.bounds.size.width
- var SCREENHeight = UIScreen.main.bounds.size.height
- let QRCodeWidth = Double(min(SCREENWidth, SCREENHeight)) / 1.5
- let RATIO = 0.45
- enum ArgumentsEnum: String {
- case title = "SCAN_TITLE"
- case laserColor = "LASER_COLOR"
- case titleColor = "TITLE_COLOR"
- case playBeep = "KEY_PLAY_BEEP"
- case scanWidth = "SCAN_WIDTH"
- case scanHeight = "SCAN_HEIGHT"
- case promptMessage = "PROMPT_MESSAGE"
- case permissionDeniedMessage = "PERMISSION_DENIED_MESSAGE"
- case confirmText = "MESSAGE_CONFIRM_TEXT"
- case cancelText = "MESSAGE_CANCEL_TEXT"
- func getKeyValue<T>(dictionary: NSDictionary) -> T? {
- guard let result = dictionary[self.rawValue] as? T else {
- return nil
- }
- return result
- }
- }
- protocol ScannerDelegate: class {
- func didScanWithResult(code: String)
- func didFailWithErrorCode(code: String)
- }
- class ScannerViewController: UIViewController, AVCaptureMetadataOutputObjectsDelegate {
- @IBOutlet private var camera: UIView!
- private var session: AVCaptureSession? = nil
- private var top = 0.0
- var bundle: Bundle? = nil
- var currentOrientation = UIInterfaceOrientationMask.portrait
- weak var delegate: ScannerDelegate?
- var arguments: NSDictionary = [:]
- var laserColor: UIColor = UIColor.clear
- var promptMessage: String?
- 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."
- var confirmText: String = "OK"
- var cancelText: String = "Cancel"
- var windowOrientation: UIInterfaceOrientation {
- if #available(iOS 13.0, *) {
- return view.window?.windowScene?.interfaceOrientation ?? .unknown
- } else {
- return UIApplication.shared.statusBarOrientation
- }
- }
- required init?(coder aDecoder: NSCoder) {
- super.init(coder: aDecoder)
- }
- init() {
- super.init(nibName: nil, bundle: nil)
- let mainBundle = Bundle(for: type(of: self))
- let url = mainBundle.url(forResource: "FlutterScannerBundle", withExtension: "bundle")
- if let url = url {
- bundle = Bundle(url: url)
- }
- if(bundle == nil) {
- return
- }
- getCurrentOrientation()
- }
- override func viewDidLoad() {
- super.viewDidLoad()
- initArguments()
- setupNavigationBar()
- }
- override func viewWillAppear(_ animated: Bool) {
- super.viewWillAppear(animated)
- checkAuthorization()
- }
- override var shouldAutorotate: Bool {
- get {
- return false
- }
- }
- override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
- get {
- return currentOrientation
- }
- }
- override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
- session?.stopRunning()
- SCREENWidth = UIScreen.main.bounds.size.width
- SCREENHeight = UIScreen.main.bounds.size.height
- checkAuthorization()
- }
- func getCurrentOrientation(){
- switch UIDevice.current.orientation {
- case UIDeviceOrientation.faceDown:
- break
- case UIDeviceOrientation.unknown:
- break
- case UIDeviceOrientation.portrait:
- break
- case UIDeviceOrientation.portraitUpsideDown:
- break
- case UIDeviceOrientation.faceUp:
- break
- case UIDeviceOrientation.landscapeLeft:
- currentOrientation = UIInterfaceOrientationMask.landscapeRight
- break
- case UIDeviceOrientation.landscapeRight:
- currentOrientation = UIInterfaceOrientationMask.landscapeLeft
- break
- @unknown default:
- break
- }
- }
- func initArguments() {
- SCREENWidth = UIScreen.main.bounds.size.width
- SCREENHeight = UIScreen.main.bounds.size.height
- if let titleHex: String = ArgumentsEnum.title.getKeyValue(dictionary: arguments) {
- self.title = titleHex
- }
- if let laserColorHex: String = ArgumentsEnum.laserColor.getKeyValue(dictionary: arguments) {
- laserColor = UIColor(hexString: laserColorHex) ?? UIColor.clear
- }
- if let promptMessageHex: String = ArgumentsEnum.promptMessage.getKeyValue(dictionary: arguments) {
- promptMessage = promptMessageHex
- }
- if let permissionDeniedTextHex: String = ArgumentsEnum.permissionDeniedMessage.getKeyValue(dictionary: arguments) {
- permissionDeniedText = permissionDeniedTextHex
- }
- if let confirmTextHex: String = ArgumentsEnum.confirmText.getKeyValue(dictionary: arguments) {
- confirmText = confirmTextHex
- }
- if let cancelTextHex: String = ArgumentsEnum.cancelText.getKeyValue(dictionary: arguments) {
- cancelText = cancelTextHex
- }
- }
- func checkAuthorization(){
- /*
- Check the video authorization status. Video access is required and audio
- access is optional. If the user denies audio access, AVCam won't
- record audio during movie recording.
- */
- switch AVCaptureDevice.authorizationStatus(for: .video) {
- case .authorized:
- self.setupMaskView()
- self.beginScanning()
- break
- case .notDetermined:
- AVCaptureDevice.requestAccess(for: .video, completionHandler: { granted in
- if !granted {
- self.permissionDenied()
- }
- self.setupMaskView()
- self.beginScanning()
- })
- default:
- self.permissionDenied()
- }
- }
- func permissionDenied(){
- DispatchQueue.main.async {
- let alertController = UIAlertController(title: self.title, message: self.permissionDeniedText, preferredStyle: .alert)
- let confirmAction = UIAlertAction(title: self.confirmText, style: .default) { (action) in
- if let url = URL(string: UIApplication.openSettingsURLString) {
- if #available(iOS 10, *) {
- UIApplication.shared.open(url, options: [:], completionHandler: {
- (success) in
- self.dismiss(animated: true, completion: nil)
- })
- } else {
- UIApplication.shared.openURL(url)
- self.dismiss(animated: true, completion: nil)
- }
- }
- }
- let cancelAction = UIAlertAction(title: self.cancelText, style: .default) { (action) in
- self.dismiss(animated: true, completion: nil)
- }
- alertController.addAction(confirmAction)
- alertController.addAction(cancelAction)
- self.present(alertController, animated: true)
- }
- }
- func setupMaskView() {
- UINib.init(nibName: "FlutterQrScanner", bundle: bundle!).instantiate(withOwner: self, options: nil)
- var scanY = Double(SCREENHeight) - top - QRCodeWidth
- scanY = scanY * RATIO
- let frame = CGRect(x: (Double(SCREENWidth) - QRCodeWidth) / 2.0, y: scanY, width: QRCodeWidth, height: QRCodeWidth)
- let backgroundView = UIView(frame: UIScreen.main.bounds)
- backgroundView.backgroundColor = UIColor.init(red: 0, green: 0, blue: 0, alpha: 0.6)
- camera.addSubview(backgroundView)
- let maskLayer = CAShapeLayer()
- maskLayer.fillRule = CAShapeLayerFillRule.evenOdd // fill rule
- let basicPath = UIBezierPath(rect: UIScreen.main.bounds) // basic
- let maskPath = UIBezierPath(roundedRect: frame, cornerRadius: 15)
- basicPath.append(maskPath) // recover
- maskLayer.path = basicPath.cgPath
- backgroundView.layer.mask = maskLayer
- let scanBorder = BorderCanvas(frame: frame, border: laserColor)
- camera.addSubview(scanBorder)
- let scanWindow = UIView(frame: frame)
- scanWindow.clipsToBounds = true
- camera.addSubview(scanWindow)
- let winMaskLayer = CAShapeLayer()
- // fill rule
- winMaskLayer.fillRule = CAShapeLayerFillRule.evenOdd
- let winFrame = CGRect(x: 0, y: 0, width: QRCodeWidth, height: QRCodeWidth)
- let winBasicPath = UIBezierPath(rect: winFrame)
- let winMaskPath = UIBezierPath(rect: winFrame)
- let winMaskPath2 = UIBezierPath(roundedRect: winFrame, cornerRadius: 15)
- winBasicPath.append(winMaskPath)
- winBasicPath.append(winMaskPath2)
- winMaskLayer.path = winBasicPath.cgPath
- scanWindow.layer.mask = winMaskLayer
- //scan window animation
- let scanNetImageViewH = scanWindow.frame.size.height
- let scanNetImageViewW = scanWindow.frame.size.width
- let scanNetImageView = UIImageView(image: UIImage.init(named: "scannet", in: bundle!, compatibleWith: nil))
- scanNetImageView.frame = CGRect(x: 0, y: -scanNetImageViewH, width: scanNetImageViewW, height: scanNetImageViewH)
- let scanNetAnimation = CABasicAnimation(keyPath: "transform.translation.y")
- scanNetAnimation.byValue = NSNumber(value: QRCodeWidth)
- scanNetAnimation.duration = 1.5
- scanNetAnimation.repeatCount = MAXFLOAT
- scanNetImageView.layer.add(scanNetAnimation, forKey: "animation")
- scanWindow.addSubview(scanNetImageView)
- let promptSize = CGSize(width: Double(SCREENWidth) - 30, height: 0)
- let promptRect = (promptMessage ?? "").boundingRect(with: promptSize, options: NSStringDrawingOptions.usesLineFragmentOrigin, attributes: nil , context: nil)
- let promptLabel = UILabel(frame: CGRect(x: 15, y: scanY + QRCodeWidth + 30, width: Double(SCREENWidth) - 30, height: Double(promptRect.size.height)))
- promptLabel.textColor = UIColor.gray
- promptLabel.text = promptMessage
- promptLabel.textAlignment = NSTextAlignment.center
- camera.addSubview(promptLabel)
- }
- func setupNavigationBar(){
- let navHeight = self.navigationController?.navigationBar.bounds.height ?? 20
- let backButton = UIButton(frame: CGRect(x: 0, y: 0, width: navHeight, height: navHeight))
- backButton.setImage(UIImage.init(named: "arrow_left", in: bundle!, compatibleWith: nil), for: .normal)
- backButton.setTitle("", for: .normal)
- backButton.setTitleColor(backButton.tintColor, for: .normal)
- backButton.addTarget(self, action: #selector(backButtonPressed), for: .touchUpInside)
- backButton.imageView?.contentMode = .scaleAspectFit
- self.navigationItem.leftBarButtonItem = UIBarButtonItem(customView: backButton)
- self.navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default)
- self.navigationController?.navigationBar.shadowImage = UIImage()
- self.navigationController?.navigationBar.isTranslucent = true
- self.navigationController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white, NSAttributedString.Key.font: UIFont.systemFont(ofSize:18)]
- }
- func beginScanning() {
- //get device
- let device = AVCaptureDevice.default(for: .video)
- //create device input
- var input: AVCaptureDeviceInput? = nil
- do {
- if let device = device {
- input = try AVCaptureDeviceInput(device: device)
- }
- } catch {
- delegate?.didFailWithErrorCode(code: "")
- return
- }
- if(input == nil){
- delegate?.didFailWithErrorCode(code: "")
- return
- }
- //create device output
- let output = AVCaptureMetadataOutput()
- let xx = (Double(SCREENHeight) - QRCodeWidth - top) * RATIO
- let x = xx / Double(SCREENHeight)
- let yy = (Double(SCREENWidth) - QRCodeWidth) / 2.0
- let y = yy / Double(SCREENWidth)
- let width = QRCodeWidth / Double(SCREENHeight)
- let height = QRCodeWidth / Double(SCREENWidth)
- output.rectOfInterest = CGRect(x: x, y: y, width: width, height: height)
- output.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
- session = AVCaptureSession()
- session!.sessionPreset = .high
- if session?.canAddInput(input!) ?? false {
- session!.addInput(input!)
- session!.addOutput(output)
- //code data type
- output.metadataObjectTypes = [
- .qr,
- .ean13,
- .ean8,
- .code128
- ]
- let layer = AVCaptureVideoPreviewLayer(session: session!)
- layer.frame = CGRect(x: 0, y: 0, width: CGFloat(SCREENWidth), height: SCREENHeight - CGFloat(top))
- layer.videoGravity = .resizeAspectFill
- camera.layer.insertSublayer(layer, at: 0)
- DispatchQueue.main.async {
- /*
- Dispatch video streaming to the main queue because AVCaptureVideoPreviewLayer is the backing layer for PreviewView.
- You can manipulate UIView only on the main thread.
- Note: As an exception to the above rule, it's not necessary to serialize video orientation changes
- on the AVCaptureVideoPreviewLayer’s connection with other session manipulation.
- Use the window scene's orientation as the initial video orientation. Subsequent orientation changes are
- handled by CameraViewController.viewWillTransition(to:with:).
- */
- var initialVideoOrientation: AVCaptureVideoOrientation = .portrait
- if self.windowOrientation != .unknown {
- if let videoOrientation = AVCaptureVideoOrientation(rawValue: self.windowOrientation.rawValue) {
- initialVideoOrientation = videoOrientation
- }
- }
- layer.connection?.videoOrientation = initialVideoOrientation
- }
- //start
- session!.startRunning()
- } else {
- print("Couldn't add video device input to the session.")
- session!.commitConfiguration()
- return
- }
- }
- func metadataOutput(_ captureOutput: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
- if metadataObjects.count > 0 {
- //code result data
- let metadataObject = metadataObjects[0] as? AVMetadataMachineReadableCodeObject
- delegate?.didScanWithResult(code: metadataObject?.stringValue ?? "")
- self.dismiss(animated: true, completion: nil)
- }
- }
- @objc func backButtonPressed() {
- delegate?.didFailWithErrorCode(code: "canceled")
- self.dismiss(animated: true, completion: nil)
- }
- override func viewWillDisappear(_ animated: Bool) {
- super.viewWillDisappear(animated)
- self.session?.stopRunning()
- }
- }
- class BorderCanvas: UIView {
- var border: UIColor = UIColor.clear
- override init(frame: CGRect) {
- super.init(frame: frame)
- self.backgroundColor = UIColor.clear
- }
- convenience init(frame: CGRect, border: UIColor?) {
- self.init(frame: frame)
- self.border = border ?? UIColor.clear
- }
- required init?(coder aDecoder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
- override func draw(_ rect: CGRect) {
- let pathRect = self.bounds.insetBy(dx: 1, dy: 1)
- let path = UIBezierPath(roundedRect: pathRect, cornerRadius: 15)
- path.lineWidth = 3
- UIColor.clear.setFill()
- self.border.setStroke()
- path.fill()
- path.stroke()
- let maskLayer = CAShapeLayer()
- maskLayer.fillRule = CAShapeLayerFillRule.evenOdd // fill rule
- let basicPath = UIBezierPath(rect: CGRect(x: 0, y: 0, width: QRCodeWidth, height: QRCodeWidth)) // basic
- let maskPath = UIBezierPath(rect: CGRect(x: QRCodeWidth / 6, y: 0, width: QRCodeWidth * 2 / 3, height: QRCodeWidth))
- let maskPath2 = UIBezierPath(rect: CGRect(x: 0, y: QRCodeWidth / 6, width: QRCodeWidth, height: QRCodeWidth * 2 / 3))
- basicPath.append(maskPath) // recover
- basicPath.append(maskPath2) // recover
- maskLayer.path = basicPath.cgPath
- layer.mask = maskLayer
- }
- }
- extension AVCaptureVideoOrientation {
- init?(deviceOrientation: UIDeviceOrientation) {
- switch deviceOrientation {
- case .portrait: self = .portrait
- case .portraitUpsideDown: self = .portraitUpsideDown
- case .landscapeLeft: self = .landscapeRight
- case .landscapeRight: self = .landscapeLeft
- default: return nil
- }
- }
- init?(interfaceOrientation: UIInterfaceOrientation) {
- switch interfaceOrientation {
- case .portrait: self = .portrait
- case .portraitUpsideDown: self = .portraitUpsideDown
- case .landscapeLeft: self = .landscapeLeft
- case .landscapeRight: self = .landscapeRight
- default: return nil
- }
- }
- }
- extension UIColor {
- convenience init(rgba: Int) {
- self.init(
- red: CGFloat((rgba & 0x00FF0000) >> 16) / 255.0,
- green: CGFloat((rgba & 0x0000FF00) >> 8) / 255.0,
- blue: CGFloat(rgba & 0x000000FF) / 255.0,
- alpha: CGFloat((rgba & 0xFF000000) >> 24) / 255.0
- )
- }
- convenience init?(hexString: String) {
- var chars = Array(hexString.hasPrefix("#") ? hexString.dropFirst() : hexString[...])
- let red, green, blue, alpha: CGFloat
- switch chars.count {
- case 3:
- chars = chars.flatMap { [$0, $0] }
- fallthrough
- case 6:
- chars = ["F","F"] + chars
- fallthrough
- case 8:
- alpha = CGFloat(strtoul(String(chars[0...1]), nil, 16)) / 255
- red = CGFloat(strtoul(String(chars[2...3]), nil, 16)) / 255
- green = CGFloat(strtoul(String(chars[4...5]), nil, 16)) / 255
- blue = CGFloat(strtoul(String(chars[6...7]), nil, 16)) / 255
- default:
- return nil
- }
- self.init(red: red, green: green, blue: blue, alpha: alpha)
- }
- }
|