PUROGU LADESU

ポエムがメインのブログです。

【Swift】QRコードを読み取る

QRコードリーダー

f:id:memorude:20210127004017p:plain

AVFoundationを使います。
画像や音声を取得できるライブラリです。今回はmetadataを使ってバーコード認識のみ行います。

metadataObjectTypesで認識するバーコードを指定します。
バーコードを読み取ったらdidOutput metadataObjectsが呼ばれます。

rectOfInterestを指定してその範囲内のみ認識させるようにします。分かるように画面上に赤枠を出しますが、rectOfInterestは0.0~1.0の割合で指定する必要があるのでややこしい。今回は赤枠の位置を先に決めてから割合を計算しました。

ここがハマリポイントですが、デフォルトが水平になっているようなので、計算したあと縦横を入れ替える必要があります。

class BarcodeReaderViewController: UIViewController {

    var captureSession = AVCaptureSession()
    var currentDevice: AVCaptureDevice?
    var metadataOutput: AVCaptureMetadataOutput!
    var cameraPreviewLayer: AVCaptureVideoPreviewLayer!
    var outputString = ""
    var detectionArea: CGRect!
    var areaOfInterest: CGRect! // 0.0-1.0の範囲
    
    var authStatus: AuthorizedStatus = .authorized
    enum AuthorizedStatus {
        case authorized
        case notAuthorized
        case failed
    }
    
    static func fromStoryboard() -> BarcodeReaderViewController? {
        return UIStoryboard(name: "Main", bundle: nil)
            .instantiateViewController(withIdentifier: "BarcodeReaderViewController") as? BarcodeReaderViewController
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()

        setupCloseButton()
        setupMessage()
        
        cameraAuth()
        if authStatus != .authorized {
            dismiss(animated: true)
            return
        }
        
        setupDetectionArea()
        setupDevice()
        setupInputOutput()
        setupPreviewLayer()
        
        DispatchQueue.global(qos: .userInitiated).async {
            self.captureSession.startRunning()
        }
        
    }
    
    
    // 認識エリアの計算
    func setupDetectionArea() {
        // まず赤枠のサイズを決める(横幅の70%のサイズの正方形を上から30%の位置に表示)
        detectionArea = CGRect(x: view.bounds.width * 0.15, y: view.bounds.height * 0.3, width: view.bounds.width * 0.7, height: view.bounds.width * 0.7)
        // 画面の大きさで割って割合を計算
        let interest = CGRect(x: detectionArea.origin.x / view.bounds.width, y: detectionArea.origin.y / view.bounds.height, width: detectionArea.width / view.bounds.width, height: detectionArea.height / view.bounds.height)
        // 縦横を入れ替える
        areaOfInterest = CGRect(x: interest.origin.y, y: interest.origin.x, width: interest.height, height: interest.width) // 縦横入れ替え
    }
    
    // カメラの準備
    func setupDevice() {
        // 通常広角カメラの背面カメラを探す
        let deviceDiscoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: .back)
        let devices = deviceDiscoverySession.devices
        
        for device in devices {
            currentDevice = device
            break
        }
    }
   
    // 入力と出力の設定
    func setupInputOutput() {
        guard let device = currentDevice else {
            print("No camera available")
            return
        }
        
        do {
            // 入力
            let captureDeviceInput = try AVCaptureDeviceInput(device: device)
            captureSession.addInput(captureDeviceInput)
            // 出力
            metadataOutput = AVCaptureMetadataOutput()
            metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
            captureSession.addOutput(metadataOutput)
            // QRコードを認識します。addOutputより後ろで設定すること
            metadataOutput.metadataObjectTypes = [.qr]
            // 認識エリアを指定する
            metadataOutput.rectOfInterest = areaOfInterest
            
        } catch {
            print(error)
        }
    }

    // バーコード探索中のプレビュー表示
    func setupPreviewLayer() {
        // 背面全部を映す
        cameraPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
        cameraPreviewLayer.videoGravity = .resizeAspectFill
        cameraPreviewLayer.connection?.videoOrientation = .portrait
        cameraPreviewLayer.frame = view.bounds
        view.layer.insertSublayer(cameraPreviewLayer, at: 0)
 
        // 認識エリアに赤枠を出す
        let borderView = UIView(frame: detectionArea)
        borderView.layer.borderWidth = 2
        borderView.layer.borderColor = UIColor.red.cgColor
        view.addSubview(borderView)
    }

    
    func foundBarcode() {
        captureSession.stopRunning()
        self.dismiss(animated: true) 
    }
    
}

// MARK: - AVCaptureMetadataOutputObjectsDelegate

extension BarcodeReaderViewController: AVCaptureMetadataOutputObjectsDelegate {
    
    func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
        for metadataObject in metadataObjects {
            if let metadata = metadataObject as? AVMetadataMachineReadableCodeObject {
                if metadata.type == .qr {
                    if let string = metadata.stringValue {
                        // 文字列が取れたら画面を閉じます
                        outputString = string
                        self.foundBarcode()
                    }
                }
            }
        }
    }
    
}

他の部分のコード

extension BarcodeReaderViewController {
    func cameraAuth() {
        let status = AVCaptureDevice.authorizationStatus(for: .video)
        switch status {
        case .notDetermined:
            // 初回起動時、許可を求める
            AVCaptureDevice.requestAccess(for: .video) {[unowned self] authorized in
                print("初回", authorized.description)
                if authorized {
                    self.authStatus = .authorized
                } else {
                    self.authStatus = .notAuthorized
                }
            }
        case .restricted, .denied:
            authStatus = .notAuthorized
        case .authorized:
            authStatus = .authorized
        @unknown default:
            fatalError()
        }
    }
    func setupCloseButton() {
        let button = UIButton()
        button.setTitle("✗", for: .normal)
        button.tintColor = UIColor.white
        button.backgroundColor = UIColor(white: 0.1, alpha: 0.8)
        button.layer.cornerRadius = 16
        button.addTarget(self, action: #selector(tapClose(_:)), for: .touchUpInside)
        view.addSubview(button)
        
        button.translatesAutoresizingMaskIntoConstraints = false
        button.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 16).isActive = true
        button.topAnchor.constraint(equalTo: view.topAnchor, constant: 32).isActive = true
        button.widthAnchor.constraint(equalToConstant: 32).isActive = true
        button.heightAnchor.constraint(equalToConstant: 32).isActive = true
    }
    
    @objc func tapClose(_ button: UIButton) {
        dismiss(animated: true)
    }

    func setupMessage() {
        let label = UILabel()
        label.text = "QRコードを読み取ってください"
        label.numberOfLines = 2
        label.textColor = UIColor.white
        view.addSubview(label)
        
        label.translatesAutoresizingMaskIntoConstraints = false
        label.topAnchor.constraint(equalTo: view.topAnchor, constant: 128).isActive = true
        label.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 32).isActive = true
        label.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 32).isActive = true
    }