目次
概要
前回はQRコードの読み取りの実装を行った。
しかし、QRコードリーダーのアプリは範囲が指定されているものが多い。今回はその範囲指定を行う。
また、実験のため、今回は読み取ったQRコードを緑色の線で囲むこともしている。
Swiftでの実装は多かったが、SwiftUIでの実装は少なかったため記載。
ソースコード
TestApp.swift
import SwiftUI
@main
struct TestApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
QrScanView()
}
}
}
QrScanView.swift
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) {
}
}
QrScanViewModel.swift
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
}
}
}
デモ動画
詳細
これはSwiftUIでかつ画面表示側と処理側に分けるとなかなか調べても出てこない。
今回重要になっているのは以下だ。
View表示側
var body: some View {
ZStack {
QrCapturePreview(qrScanViewModel: viewModel)
qrScanableArea()
}
.frame(width: viewModel.cameraArea.width,
height: viewModel.cameraArea.height)
}
private func qrScanableArea() -> some View {
return ZStack {}
.frame(width: viewModel.detectArea.width,
height: viewModel.detectArea.height)
.border(.red, width: 2)
}
まずはView表示側から。
これは単純で、空白のViewをサイズと境界線を指定して描画することだ。
qrScanableAreaメソッド内でサイズを設定するframe、境界線を設定するborderを設定することでViewを描画。
処理部分
以下は全て同じファイル内で行われている
読み取り範囲のサイズを指定
private let qrDetectAreaSize: CGFloat = 200.0
カメラから取得した映像を表示する領域(cameraArea)と、QRコードの読み取り範囲(detectArea)を指定。これらは画面に表示に関係あるもの。
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()
}
カメラ側にQRコードの読み取り範囲(rectOfInterest)を指定
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
以上でできる。
detectAreaはいわゆるデモ動画の赤枠の部分だ。
つまり、この枠の中に表示されているQRコードが読み取りの対象となるQRコードだ。
こうすることで、読み取るつもりのなかったQRコードの読み取り処理を防ぐのに使える。
しかし、これだけでは表示だけのお飾りにすぎない。
そこで必要になるのが、rectOfInterestに読み取り範囲を設定すること。
こうすることでQRコードの読み取り領域を設定するのだが、
注意点として、これは水平方向、垂直方向の値の範囲が共に「0.0~1.0」になっていること。
つまり、上記の処理のように位置、大きさをカメラの映像を表示しているサイズで割ってあげる必要がある。
もう一つの注意点として、x, yの指定が逆になっていること。
コードを見ればわかるが、
水平方向(x, width)に関する部分に垂直方向の情報(y, height)が、
垂直方向(y, height)に関する部分に本来水平方向の情報(x, width)が格納されている。
これはこの領域の設定が画面を横向きにしていることを前提にしているからだ。
上記の点させ注意すればQRコード読み取り部分のしてもできる。
読み取ったQRコードを緑色の線で囲む点はまた次回記載しよう。