Swift4 QRコードの読み取りと生成を行なう

今回、QRCoderという名前でQRコードを読み取るかつ生成するという簡単なクラスを作ってみました。 主に、使ったLibraryはRxSwiftSwinJectです。

Protocolの作成

まずは、コードから

protocol QRCoderDelegate {
    func qrCoder(_ qrCoder: QRCoder, didDetectQRCode url: URL)
}

まず、QRコードの読み取りが終わったことを伝えるdelegate処理を行います。このdelegate処理が伝えられる場所では、urlを受け取りことが可能です。

protocol QRCoderProtocol {
    func configure(on view: UIView)
    func startRunning()
    func stopRunning()
    func generate(from url: String) -> UIImage?
    func scanQR(from view: UIImage) -> String?
}

上記のようにprotocolを生成するとこのprotocolに準拠するClass or Structは、このメソッドを書かなければエラーが起こります。 下のClass QRCoderは、QRCoderProtocol に準拠しています。そのため、protocolで書かれたメソッドが全て呼ばれています。

final class QRCoder: NSObject, QRCoderProtocol {
    //カメラの入出力を行うクラス
    private let captureSession = AVCaptureSession()
   //カメラに映る映像を画面に写すクラス
    private var videoPreviewLayer: AVCaptureVideoPreviewLayer? = nil
    private var delegate: QRCoderDelegate?
   
    init(delegate: QRCoderDelegate?) {
        self.delegate = delegate
    }
    
    func configure(on view: UIView) {
        configureSession()
        addVideoPreviewLayer(on: view)
    }
    //このメソッドは、captureSessionでの入出力が行われていない場合に`caputureSession.startRunning`で入出力を開始させることができます。
    func startRunning() {
        guard !captureSession.isRunning else {
            return
        }
        captureSession.startRunning()
    }
     //このメソッドは、captureSessionでの入出力が行われている場合に`caputureSession.stopRunning`で入出力を終了させることができます。
    func stopRunning() {
        guard captureSession.isRunning else {
            return
        }
        captureSession.stopRunning()
    }

    //ここで、QRコードの生成を行なっています。URLを受けっとてそれをUIImageに変換させています。
    func generate(from url: String) -> UIImage? {
  //string型をURLに直すことができるかをguardで判断している。
        guard let _  = URL(string: url) else {
            return nil
        }
        //urlをdataに変え、 "inputCorrectionLevel"として英文字を加えます。
        let parameters: [String: Any] = [
            "inputMessage": url.data(using: .utf8)!,
            "inputCorrectionLevel": "H"
        ]
        //CIFilterは、主に画像の加工などを行う際に利用されます。引数のnameに行いたい処理を書くことでその加工を行うことができます。
  //今回は、QRコードの生成ということで`CIQRCodeGenerator`と書きました。そして、Parameterも引数として渡します。
        let filter = CIFilter(name: "CIQRCodeGenerator", withInputParameters: parameters)
        guard let outputImage = filter?.outputImage else {
            return nil
        }
        //次にCIImage->CgImageに変換させる
        let scaleImage = outputImage.transformed(by: CGAffineTransform(scaleX: 10, y: 10))
        guard let cgImage = CIContext().createCGImage(scaleImage, from: scaleImage.extent) else {
            return nil
        }
        //最後に、cgImageをUIImageに変換させます。
        return UIImage(cgImage: cgImage)
    }
}

ところどころで、エラーの際はnilを返しています。ここで、nilを流すことでエラー時のハンドリング処理が行うことができます。 上記のコードを見て見ると、CIImageとCGImage、UIImageと3つのimageが出ています。 その違いについて簡単に紹介して行きたいと思います。 CIImage: CIImageは、主に画像のエフェクトをかけることができる。 Foodieなどのアプリは、Imageを受け取りCGImageに直しているのではないかと思います。

CGImage:画像の切り取りなどを行うことができます。 UIImage: UIImageでは加工などを行うことは出来ず主にViewとして使うことができる。

詳しくは、こちらのQiitam記事を参照ください。

https://qiita.com/jok/items/7c9e384abb29c161f56b

    //次にimageから QRコードを読み取りstring型で返すという処理の実装です。
    func scanQR(from view: UIImage) -> String? {
        guard let ciImage = CIImage(image: view) else {
            return nil
        }
        let detector = CIDetector(ofType: CIDetectorTypeQRCode, context: nil, options: nil)
        let features = detector?.features(in: ciImage)
        guard let castFeatures = features as? [CIQRCodeFeature] else {
            return nil
        }
        for feature in castFeatures {
            guard feature.messageString != nil else {
            return nil
            }
            return feature.messageString
        }
        return nil
    }
//以下で、QRの読み取りを行なっています。
private extension QRCoder {
   //携帯のカメラを使って処理を行うのかについて書いています。
 //以下では、背面のbuiltInWideAngleCameraでVideoを撮るという処理を行なっています。QRコードはvideoで読み取っているみたいです。
    func configureSession() {
        let deviceDiscoverySession = AVCaptureDevice
            .DiscoverySession(
                deviceTypes: [.builtInWideAngleCamera],
                mediaType: .video,
                position: .back
        )
        //infp.plistでカメラ機能の利用を許可していなければここでerrorになる仕組みです。
        guard let captureDevice = deviceDiscoverySession.devices.first else {
            fatalError("Failed to get the camera device")
        }
        
        //カメラの出力に対する処理を書いています。deviceで読み取ったものを` captureSession.addInput(input)`でinputしています。
        do {
            let input = try AVCaptureDeviceInput(device: captureDevice)
            captureSession.addInput(input)
        } catch let error {
            fatalError(error.localizedDescription)
        }
        
  //出力に対する処理を書いています。 
       //AVCaptureMetadataOutputは、指定されたメタデータ検出しその処理のためにデリゲートに転送します。
        let captureMetadataOutput = AVCaptureMetadataOutput()
        captureSession.addOutput(captureMetadataOutput)
        //ここで、データの受け渡しを行うdelegateを設定します。
        captureMetadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
       //今回行うはQRコードの読み取りなのでqrを指定します。
        captureMetadataOutput.metadataObjectTypes = [.qr]
        
        //最後にカメラからの映像を`AVCaptureVideoPreviewLayer`でviewに写すことができます。
        videoPreviewLayer = AVCaptureVideoPreviewLayer.init(session: captureSession)
        videoPreviewLayer?.videoGravity = AVLayerVideoGravity.resizeAspectFill
    }

    //上記の関数でもvideoPreviewLayerに対する処理を書きましたが、可読性をあげるため、引数のUIViewを扱う処理は別に書きます。
    func addVideoPreviewLayer(on view: UIView) {
        videoPreviewLayer?.frame = view.layer.bounds
        //UIViewにaddsubViewするのではなく今回はLayerに.addSublayerというコードを書きます。
  //Layerに処理を書くことで、 GPUを使った処理を可能とすることができるからです。
        view.layer.addSublayer(videoPreviewLayer!)
    }
}
//AVCaptureMetadataOutputから入ってきた情報に対する処理を書いて行きます。
extension QRCoder: AVCaptureMetadataOutputObjectsDelegate {
    func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
        guard metadataObjects.count != 0 else {
            return
        }
        //metadataObjectsがQRコードなのかを判断しています。
        guard let metadataObject = metadataObjects.first as? AVMetadataMachineReadableCodeObject,
            metadataObject.type == AVMetadataObject.ObjectType.qr else {
                return
        }
        guard let url = URL(string: metadataObject.stringValue!) else {
            return
        }
        //最後にdelegateを使って処理した値をdelegateで返しています。
        delegate?.qrCoder(self, didDetectQRCode: url)
    }
}

参考に↓今回の記事の GitHubです。 github.com