Swift5 QRリーダーの実装 脳死コピペ用

個人開発したアプリの宣伝
目的地が設定できる手帳のような使い心地のTODOアプリを公開しています。
Todo with Location

Todo with Location

  • Yoshiko Ichikawa
  • Productivity
  • Free

概要

Swift5でQRコードを読み込んだので作業記録。

ググれば沢山の記事がヒットするんだけど、どれもViewControllerに結構な量のコードが記載されているので、コピペするには結構しんどい(笑)

なので、コピペしやすいようにクラスファイルに落としましたとさ。という話し。

基本以下のクラスをコピペして、ViewControllerにdelegateメソッドを記述、Info.plistに利用用途を記述すれば動くと思う。

MyQRCodeReader.swift

import AVFoundation
import UIKit

class MyQRCodeReader {
    
    let captureSession = AVCaptureSession()
    let videoDevice = AVCaptureDevice.default(for: AVMediaType.video)
    var metadataOutput = AVCaptureMetadataOutput()
    var delegate:AVCaptureMetadataOutputObjectsDelegate? {
        get{
            return self.forwardDelegate
        }
        set(v){
            self.forwardDelegate = v
            self.metadataOutput.setMetadataObjectsDelegate(v, queue: DispatchQueue.main)
        }
    }
    var forwardDelegate:AVCaptureMetadataOutputObjectsDelegate?
    var preview:UIView?
    var previewLayer = AVCaptureVideoPreviewLayer()
    let qrView = UIView()

    //info.plist Privacy - Camera Usage Description:String
    func setupCamera( view:UIView, borderWidth:Int = 1, borderColor:CGColor =  UIColor.red.cgColor ){
        
        self.preview = view
        
        //デバイスからの入力
        do {
            let videoInput = try AVCaptureDeviceInput(device: self.videoDevice!) as AVCaptureDeviceInput
            self.captureSession.addInput(videoInput)
        } catch let error as NSError {
            print(error)
        }
        //出力
        self.captureSession.addOutput(self.metadataOutput)
        
        //読み込み対象タイプ
        self.metadataOutput.metadataObjectTypes = [AVMetadataObject.ObjectType.qr]

        //カメラ映像を表示
        self.cameraPreview(view)
        
        //認識QRの確認表示
        self.targetCapture( borderWidth:borderWidth, borderColor: borderColor )
        
        // 読み取り開始
        self.captureSession.startRunning()

    }
       
    private func cameraPreview( _ view:UIView ){
        //カメラ映像を画面に表示
        self.previewLayer = AVCaptureVideoPreviewLayer(session: self.captureSession)
        previewLayer.frame = view.bounds
        previewLayer.videoGravity = .resizeAspectFill
        view.layer.addSublayer(previewLayer)
    }
    
    private func targetCapture(borderWidth:Int, borderColor:CGColor){
        self.qrView.layer.borderWidth = CGFloat(borderWidth)
        qrView.layer.borderColor = borderColor
        qrView.frame = CGRect(x: 0, y: 0, width: 0, height: 0)
        if let v = self.preview {
            v.addSubview(qrView)
        }
    }
    
    //読み取り範囲の指定
    public func readRange( frame:CGRect = CGRect(x: 0.2, y: 0.3, width: 0.6, height: 0.4) ){
        
        self.metadataOutput.rectOfInterest = CGRect(x: frame.minY,y: 1-frame.minX-frame.size.width, width: frame.size.height,height: frame.size.width)
        
        let v = UIView()
        v.layer.borderWidth = 1
        v.layer.borderColor = UIColor.red.cgColor
        if let preview = self.preview {
            v.frame = CGRect(x: preview.frame.size.width * frame.minX, y:  preview.frame.size.height * frame.minY, width: preview.frame.size.width * frame.size.width, height: preview.frame.size.height * frame.size.height )
            preview.addSubview(v)
        }
    }

    func delegate(_ delegate:AVCaptureMetadataOutputObjectsDelegate){
        //オブジェクトを読み込んだ時のdelegate AVCaptureMetadataOutputObjectsDelegate.metadataOutput
        self.metadataOutput.setMetadataObjectsDelegate(delegate, queue: DispatchQueue.main)
    }
}
Info.plist

Info.plistには Privacy - Camera Usage Descriptionに"QRを読み取る為にカメラを使用します"なんてことを記述しておけば良いと思う。

delegateの記述と使用例

読み込んだQRはdelegateメソッドで渡されるのでViewController側で扱いを記述すればよい。

import UIKit
import AVFoundation

class ViewController: UIViewController {

    let myQRCodeReader = MyQRCodeReader()

    override func viewDidLoad() {
        super.viewDidLoad()
        myQRCodeReader.delegate = self
        myQRCodeReader.setupCamera(view:self.view)
        //読み込めるカメラ範囲
        myQRCodeReader.readRange()
    }
}

extension ViewController: AVCaptureMetadataOutputObjectsDelegate{
    //対象を認識、読み込んだ時に呼ばれる
    func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
        //一画面上に複数のQRがある場合、複数読み込むが今回は便宜的に先頭のオブジェクトを処理
        if let metadata = metadataObjects.first as? AVMetadataMachineReadableCodeObject{
            let barCode = myQRCodeReader.previewLayer.transformedMetadataObject(for: metadata) as! AVMetadataMachineReadableCodeObject
            //読み込んだQRを映像上で枠を囲む。ユーザへの通知。必要な時は記述しなくてよい。
            myQRCodeReader.qrView.frame = barCode.bounds
            //QRデータを表示
            if let str = metadata.stringValue {
                print(str)
            }                
        }
    }
}

少しだけ解説

カメラ起動

AVCaptureSessionはデバイスからの入力値と読み取った情報のオブジェクト出力の橋渡しをする。この記事が分かりやすい。

qiita.com

で、このあたりでカメラデバイスからの入力を受け取って、

let videoInput = try AVCaptureDeviceInput(device: self.videoDevice!) as AVCaptureDeviceInput
self.captureSession.addInput(videoInput)

このあたりで出力オブジェクトへの橋渡しを行なっている。

self.captureSession.addOutput(self.metadataOutput)        
//読み込み対象タイプ
self.metadataOutput.metadataObjectTypes = [AVMetadataObject.ObjectType.qr]

尚、読み込み対象はAVMetadataObject.ObjectType.qrのみとした。複数定義したければ、もちろん配列に複数定義しても良いし、self.metadataOutput.metadataObjectTypes = metadataOutput.availableMetadataObjectTypesと指定すればすべてのObjectTypeを含んだ配列のエイリアスとなっている。

ObjectTypeの種類は、Machine-Readable Object Types | Apple Developer Documentation で確認できる。

最後に

self.captureSession.startRunning()

とすればカメラが起動しQRのスキャン自体は裏側で行えている。はず。

カメラ映像の表示

しかしながら、カメラが写している映像が画面上に表示されないので、AVCaptureVideoPreviewLayerをカメラ映像の表示領域となるViewに被せてあげる必要がある。そのメソッドがこれ。

private func cameraPreview( _ view:UIView ){
    //カメラ映像を画面に表示
    self.previewLayer = AVCaptureVideoPreviewLayer(session: self.captureSession)
    previewLayer.frame = view.bounds
    previewLayer.videoGravity = .resizeAspectFill
    view.layer.addSublayer(previewLayer)
}

引数となるUIViewはViewController側からCGRect指定されたViewを渡せばよい。privateメソッドだけど、func setupCameraのFacadeになっているだけなので、setupCameraにUIViewを渡せばよい。

これでカメラ映像が表示される。

認識したQRの確認枠表示

f:id:letitride:20191203122733p:plain:h400

要は内側の赤い枠線。読み込んだQRに沿って表示される。borderWidthと色を変更できるようにした。

private func targetCapture(borderWidth:Int, borderColor:CGColor){
    self.qrView.layer.borderWidth = CGFloat(borderWidth)
    qrView.layer.borderColor = borderColor
    qrView.frame = CGRect(x: 0, y: 0, width: 0, height: 0)
    if let v = self.preview {
        v.addSubview(qrView)
    }
}

これもFacadeなので、setupCameraにborderWidth:Int, borderColor:CGColorを指定すればよい。表示不要な場合は、borderWidthに0を指定すれば表示されないんじゃないかな。

読み取り範囲の指定

カメラの端から端まで読み取り範囲になるので敢えて中央付近等、範囲を絞って対象を捕捉したい場合。要は上記画像の外側の赤枠。外側の範囲外のQRは無視される。必要ない時はメソッドを呼び出さなければよい。実装上、カメラ映像の上にaddSubviewしないといけないので、先にsetupCamera()を実行しておく必要がある(笑)。大変イケてない(笑)

あと、力尽きて枠幅と色はハードコードしてしまったので、カスタマイズは適宜変更して下さい。

public func readRange( frame:CGRect = CGRect(x: 0.2, y: 0.3, width: 0.6, height: 0.4) ){
        
    self.metadataOutput.rectOfInterest = CGRect(x: frame.minY,y: 1-frame.minX-frame.size.width, width: frame.size.height,height: frame.size.width)
        
    let v = UIView()
    v.layer.borderWidth = 1
    v.layer.borderColor = UIColor.red.cgColor
    if let preview = self.preview {
        v.frame = CGRect(x: preview.frame.size.width * frame.minX, y:  preview.frame.size.height * frame.minY, width: preview.frame.size.width * frame.size.width, height: preview.frame.size.height * frame.size.height )
        preview.addSubview(v)
    }
}

引数のCGRectで範囲を指定できるようにした。

また、metadataOutput.rectOfInterest:CGRectで読み取り範囲を指定するのだが、このプロパティには注意点が2つある。

  1. 座標値、絶対値ではなく割合値(0.0〜1.0)を指定。
    • (0,0)から何割進んだ位置から何割のサイズ描画するか
    • CGRect(x: 0.2, y: 0.3, width: 0.6, height: 0.4)の場合、x:2割、y:3割の場所からwidth6割、height4割の長さを描画
    • 敢えて上端だけ読み取りたいとかじゃない限り、 x * 2 + width = 1.0、y * 2 + height = 1.0とかにすれば良いんじゃないかな。
  2. x、yの原点とwidth、heightの定義が90度右回転した定義になっている。
    • ここを見れば分かりやすい。 iOSのバーコードリーダーで読み取り範囲を設定する - Qiita
    • 特にyにつては右端が原点0になるので注意。
    • なので、(x: frame.minY,y: 1-frame.minX-frame.size.width, width: frame.size.height,height: frame.size.width)この記述はコピペミスのようで実は正しい記述。
    • 引数自体は分かりやすいようにxは左端、yは上端、width:横幅、height:高さを指定するようにする

とりあえず、クラスファイルをコピペすれば動くと思いますよ。