目次
概要
QRコードを読み取る実装は前回のページに記載した。
ただ、どのQRコードを読み取ったか現在ではわからない。
そのため、読み取ったQRコードにエフェクトをつけて、どのQRコードを読み取ったか視覚的にわかるようにしよう。
ちなみに、このコードで読み取り対象となる範囲に入っている複数のQRコードに対してエフェクトをつけられる。
今回のエフェクトは緑色の線だ。
ソースコード
アプリ実行部分
import SwiftUI
@main
struct TestApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
QrScanView()
}
}
}
View表示部分
import SwiftUI
struct QrScanView: View {
@ObservedObject private(set) var viewModel = QrScanViewModel()
var body: some View {
ZStack {
QrCapturePreview(qrScanViewModel: viewModel)
qrFrame()
qrScanableArea()
}
.frame(width: viewModel.cameraArea.width,
height: viewModel.cameraArea.height)
}
private func qrFrame() -> some View {
return Path { path in
for readQrcodeArea in viewModel.readQrcodeArea {
path.move(to: readQrcodeArea[0])
for (index, _) in readQrcodeArea.enumerated() {
let readIndex = (index + 1) % readQrcodeArea.count
path.addLine(to: readQrcodeArea[readIndex])
}
}
}
.stroke(lineWidth: 4)
.fill(Color.green)
}
private func qrScanableArea() -> some View {
return ZStack {}
.frame(width: viewModel.detectArea.width,
height: viewModel.detectArea.height)
.border(.red, width: 2)
}
}
struct QrCapturePreview: UIViewRepresentable {
private var qrScanViewModel: QrScanViewModel
init(qrScanViewModel: QrScanViewModel) {
self.qrScanViewModel = qrScanViewModel
}
public func makeUIView(context: Context) -> some UIView {
let cameraView = qrScanViewModel.previewView
cameraView.frame = UIScreen.main.bounds
self.qrScanViewModel.launchQrcodeReader()
return cameraView
}
func updateUIView(_ uiView: UIViewType, context: Context) {
}
}
処理部分
import AVFoundation
import Foundation
import UIKit
class QrScanViewModel: NSObject, ObservableObject {
@Published var isShownDialog = false
@Published private(set) var qrString = ""
@Published private(set) var readQrcodeArea: [[CGPoint]] = []
var previewView: UIView = UIView()
let cameraArea: CGSize
private let metadataOutput = AVCaptureMetadataOutput()
private let qrDetectAreaSize: CGFloat = 200.0
private(set) var detectArea: CGRect
private var session: AVCaptureSession?
private var videoPreviewLayer: AVCaptureVideoPreviewLayer
override init() {
self.cameraArea = CGSize(width: UIScreen.main.bounds.width - 80,
height: UIScreen.main.bounds.height - 180)
self.detectArea = CGRect(x: (cameraArea.width - qrDetectAreaSize) * 0.5,
y: (cameraArea.height - qrDetectAreaSize) * 0.5,
width: qrDetectAreaSize,
height: qrDetectAreaSize)
self.videoPreviewLayer = AVCaptureVideoPreviewLayer()
super.init()
}
func launchQrcodeReader() {
if let session = self.session {
DispatchQueue.global(qos: .background).async {
session.startRunning()
}
return
}
let session = AVCaptureSession()
guard let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back),
let deviceInput = try? AVCaptureDeviceInput(device: device) else {
return
}
switch UIDevice.current.userInterfaceIdiom {
case .phone:
session.sessionPreset = .high
default:
session.sessionPreset = .photo
}
if session.canAddInput(deviceInput) {
session.addInput(deviceInput)
}
if session.canAddOutput(metadataOutput) {
session.addOutput(metadataOutput)
}
metadataOutput.metadataObjectTypes = [.qr]
metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
showCameraView(session: session)
DispatchQueue.global(qos: .background).async {
session.startRunning()
}
self.session = session
}
private func showCameraView(session: AVCaptureSession) {
DispatchQueue.main.async {
self.videoPreviewLayer = AVCaptureVideoPreviewLayer(session: session)
self.videoPreviewLayer.videoGravity = .resizeAspectFill
self.videoPreviewLayer.connection?.videoOrientation = .portrait
self.videoPreviewLayer.frame = self.previewView.bounds
let metadataOutputRectOfInterest = CGRect(x: self.detectArea.minY / self.cameraArea.height ,
y: self.detectArea.minX / self.cameraArea.width,
width: self.detectArea.height / self.cameraArea.height,
height: self.detectArea.width / self.cameraArea.width)
self.metadataOutput.rectOfInterest = metadataOutputRectOfInterest
self.previewView.layer.insertSublayer(self.videoPreviewLayer, at: 0)
}
}
}
extension QrScanViewModel: AVCaptureMetadataOutputObjectsDelegate {
func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
self.readQrcodeArea.removeAll()
for metadataObject in metadataObjects {
guard let machineReadableCode = metadataObject as? AVMetadataMachineReadableCodeObject,
machineReadableCode.type == .qr,
let stringValue = machineReadableCode.stringValue else {
return
}
guard let transformedObject = self.videoPreviewLayer.transformedMetadataObject(for: metadataObject) as? AVMetadataMachineReadableCodeObject else {
return
}
self.readQrcodeArea.append(transformedObject.corners)
self.qrString = stringValue
}
}
}
デモ動画
詳細
基本的にコードは前回と同じ。
ただ、今回はQRコードを読み取った時に読み取ったQRコードを四角で囲う。
今回は四角で囲んだが、半透明の四角形で塗りつぶし、URLのポップアップなど、そのエフェクトは自由。
読み取ったバーコードを四角で囲う
var body: some View {
ZStack {
QrCapturePreview(qrScanViewModel: viewModel)
qrFrame()
qrScanableArea()
DialogView()
}
.frame(width: viewModel.cameraArea.width,
height: viewModel.cameraArea.height)
}
private func qrFrame() -> some View {
return Path { path in
for readQrcodeArea in viewModel.readQrcodeArea {
path.move(to: readQrcodeArea[0])
for (index, _) in readQrcodeArea.enumerated() {
let readIndex = (index + 1) % readQrcodeArea.count
path.addLine(to: readQrcodeArea[readIndex])
}
}
}
.stroke(lineWidth: 4)
.fill(Color.green)
}
まずはViewの表示部分から。
こちらは線を引いて描画している。処理部分の方で取得したQRコードの四隅の座標(後述)を元に線を描画している。
中身は簡単。
まず、以下のようにPathで囲む。このPathの中に線を描画処理を描いていく。
Path { path in
// ここに描画する
}
そして、まずは以下のようにpath.moveメソッドで始点となる座標を設定する。
path.move(to: readQrcodeArea[0])
そして、path.addLineメソッドを使って、線を描画していく。
path.addLine(to: readQrcodeArea[readIndex])
こうすることでQRコードを四角く囲っている。
そして、色や線の太さは以下のように設定している。
.stroke(lineWidth: 4)
.fill(Color.green)
では、今度はQRコードの四隅の座標を取得しよう。
まず、QRコードを認識したタイミングで描画処理を行うので、処理を書く場所はmetadataOutputメソッドの中だ。
@Published private(set) var readQrcodeArea: [[CGPoint]] = []
func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
self.readQrcodeArea.removeAll()
for metadataObject in metadataObjects {
guard let machineReadableCode = metadataObject as? AVMetadataMachineReadableCodeObject,
machineReadableCode.type == .qr,
let stringValue = machineReadableCode.stringValue else {
return
}
guard let transformedObject = self.videoPreviewLayer.transformedMetadataObject(for: metadataObject) as? AVMetadataMachineReadableCodeObject else {
return
}
self.readQrcodeArea.append(transformedObject.corners)
self.qrString = stringValue
}
}
この中で大事なのは以下の部分だ。
guard let transformedObject = self.videoPreviewLayer.transformedMetadataObject(for: metadataObject) as? AVMetadataMachineReadableCodeObject else {
return
}
self.readQrcodeArea.append(transformedObject.corners)
まず、カメラを取得している映像のオブジェクトのtransformedMetadataObjectメソッドを使用してmetadataObjectの座標を取得する。
そして、取得した情報をPublishedで宣言した読み取ったQRコードの座標を格納する。
Publishedにしておけば、値が変更された時にView側でその更新された値でViewの描画を更新してくれる。