【備忘録】iOSでQRコードを読み取る

# 1. はじめに
こちらの記事を丸パクリさせていただきました。ありがとうございます。

https://www.letitride.jp/entry/2019/12/03/125802

少し変更を加えて備忘録としてここに残します。

# 2. info.plist
`info.plist`に`Privacy - Camera Usage Description`を追加します。
値にはカメラを使う説明？を入れときます。自分は「カメラを使います。」といれときました。

# 3. MyQRCodeReader.swift
QRコード読み取りを司るクラスを作ります。

```swift
import AVFoundation
import UIKit
import Foundation

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()

    }
    
    func restartCamera() {
        self.captureSession.startRunning()
        qrView.frame = CGRect(x: 0, y: 0, width: 0, height: 0)
    }
    
    func stopCamera() {
        self.captureSession.stopRunning()
        qrView.frame = CGRect(x: 0, y: 0, width: 0, height: 0)
    }
       
    private func cameraPreview( _ view:UIView ){
        //カメラ映像を画面に表示
        self.previewLayer = AVCaptureVideoPreviewLayer(session: self.captureSession)
        previewLayer.frame = view.bounds
        previewLayer.videoGravity = .resizeAspectFill
        if let orientation = self.convertUIOrientation2VideoOrientation(f: {return self.appOrientation()}) {
            previewLayer.connection!.videoOrientation = orientation
        }
        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.3, y: 0.3, width: 0.4, 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 = 5
        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)
    }
    
    func appOrientation() -> UIInterfaceOrientation {
        return UIApplication.shared.statusBarOrientation
    }

    // UIInterfaceOrientation -> AVCaptureVideoOrientationにConvert
    func convertUIOrientation2VideoOrientation(f: () -> UIInterfaceOrientation) -> AVCaptureVideoOrientation? {
        let v = f()
        switch v {
        case UIInterfaceOrientation.unknown:
                return nil
            default:
                return ([
                    UIInterfaceOrientation.portrait: AVCaptureVideoOrientation.portrait,
                    UIInterfaceOrientation.portraitUpsideDown: AVCaptureVideoOrientation.portraitUpsideDown,
                    UIInterfaceOrientation.landscapeLeft: AVCaptureVideoOrientation.landscapeLeft,
                    UIInterfaceOrientation.landscapeRight: AVCaptureVideoOrientation.landscapeRight
                ])[v]
        }
    }
}
```

先ほど紹介した記事をそのままコピペし、画面の回転を考慮するように変えました。また、カメラをストップしたりリスタートする関数も入れときました。

# 4. ViewCntroller.swift
QR読み取りを行うViewControllerでMyQRCodeReaderを使います。

```swift

import UIKit
import AVFoundation

class ViewController: UIViewController  {
    
    let myQRCodeReader = MyQRCodeReader()
    
    private var inputQrCodeID: String?
    var videoDataOutputEnable = false
    var isFirstAppear = true
    
    override func viewDidLoad() {
        super.viewDidLoad()
        videoDataOutputEnable = true
        myQRCodeReader.delegate = self
        myQRCodeReader.setupCamera(view:self.view)
        //読み込めるカメラ範囲
        myQRCodeReader.readRange()
    }
    
    // NavigationControllerを導入してると戻ってくるときに前の画面が残るのでリスタートさせる
    override func viewWillAppear(_ animated: Bool) {
        if (!isFirstAppear) {
            myQRCodeReader.restartCamera()
        }
        isFirstAppear = false
        videoDataOutputEnable = true
    }
    // カメラを止めたい時
    func stopCamera() {
        myQRCodeReader.stopCamera()
    }
}


extension ViewController: AVCaptureMetadataOutputObjectsDelegate{
    //対象を認識、読み込んだ時に呼ばれる
    func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
        if (!videoDataOutputEnable){ return }
        videoDataOutputEnable = false
        //一画面上に複数の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)
                inputQrCodeID = str
                // 読み取り完了
            }
        }
    }
}
```

`videoDataOutputEnable`で読み取りの有無を管理しています。放っておくと一秒間に何回も読み取ってしまうので一回読み取ったら後の読み込みは破棄するようにしてます。こうすることで読み込みするとすぐに次の画面に遷移できます。

全ての読み取り結果を処理したい時は`if (!videoDataOutputEnable){ return }`を削除してください。

`myQRCodeReader.setupCamera(view:self.view)`で全画面にpreview画面を表示してますが、任意のViewに変えることができます。

今後は読み取り時にアニメーションをするようにしたいですね。

# 4. さいごに
AndroidだとZxingで簡単にできるんですが...ちょっとクセある感じです。