概要
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
はデバイスからの入力値と読み取った情報のオブジェクト出力の橋渡しをする。この記事が分かりやすい。
で、このあたりでカメラデバイスからの入力を受け取って、
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の確認枠表示
要は内側の赤い枠線。読み込んだ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つある。
- 座標値、絶対値ではなく割合値(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とかにすれば良いんじゃないかな。
- 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:高さを指定するようにする
とりあえず、クラスファイルをコピペすれば動くと思いますよ。