はじめに
SwiftUIでQRコードを読み取るアプリ
SwiftUIでQRコード読み取り箇所は下記を参考にしました。
https://blog.devgenius.io/camera-preview-and-a-qr-code-scanner-in-swiftui-48b111155c66
本記事では、シュミレータでの確認処理など取り除いてあるので、
必要な方はリンク先を参照して下さい。
環境
macOS Big Sur 11.3
XCode 12.5
Swift 5
アプリの概要
(1).カメラ起動をタップして、QRコード読み取り画面を表示する。
(2).QRコード読み取りが成功したら、最初の画面に戻る。
(3).QRコードで読み取った文字列を表示する。
QRコード読み取り関連のクラス
Viewの状態を保持するModel
import Foundation
class ScannerViewModel: ObservableObject {
/// QRコードを読み取る時間間隔
let scanInterval: Double = 1.0
@Published var lastQrCode: String = "QRコード"
@Published var isShowing: Bool = false
/// QRコード読み取り時に実行される。
func onFoundQrCode(_ code: String) {
self.lastQrCode = code
isShowing = false
}
}
QRコードが見つかったかどうかをチェックして、そのQRコードの値を親Viewに通知するデリゲート
import AVFoundation
class QrCodeCameraDelegate: NSObject, AVCaptureMetadataOutputObjectsDelegate {
var scanInterval: Double = 1.0
var lastTime = Date(timeIntervalSince1970: 0)
var onResult: (String) -> Void = { _ in }
var mockData: String?
func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
if let metadataObject = metadataObjects.first {
guard let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject else { return }
guard let stringValue = readableObject.stringValue else { return }
foundBarcode(stringValue)
}
}
@objc func onSimulateScanning(){
foundBarcode(mockData ?? "Simulated QR-code result.")
}
func foundBarcode(_ stringValue: String) {
let now = Date()
if now.timeIntervalSince(lastTime) >= scanInterval {
lastTime = now
self.onResult(stringValue)
}
}
}
カメラの映像を表示するためのUIViewです。
これはUIKitのビューで、最終的にはSwiftUIのレイアウトでUIViewRepresentableを使って表示されます。
import UIKit
import AVFoundation
class CameraPreview: UIView {
private var label:UILabel?
var previewLayer: AVCaptureVideoPreviewLayer?
var session = AVCaptureSession()
weak var delegate: QrCodeCameraDelegate?
init(session: AVCaptureSession) {
super.init(frame: .zero)
self.session = session
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc func onClick(){
delegate?.onSimulateScanning()
}
override func layoutSubviews() {
super.layoutSubviews()
previewLayer?.frame = self.bounds
}
}
QRコード読み取りView
import SwiftUI
import AVFoundation
struct QrCodeScannerView: UIViewRepresentable {
var supportedBarcodeTypes: [AVMetadataObject.ObjectType] = [.qr]
typealias UIViewType = CameraPreview
private let session = AVCaptureSession()
private let delegate = QrCodeCameraDelegate()
private let metadataOutput = AVCaptureMetadataOutput()
func interval(delay: Double) -> QrCodeScannerView {
delegate.scanInterval = delay
return self
}
func found(r: @escaping (String) -> Void) -> QrCodeScannerView {
print("found")
delegate.onResult = r
return self
}
func setupCamera(_ uiView: CameraPreview) {
if let backCamera = AVCaptureDevice.default(for: AVMediaType.video) {
if let input = try? AVCaptureDeviceInput(device: backCamera) {
session.sessionPreset = .photo
if session.canAddInput(input) {
session.addInput(input)
}
if session.canAddOutput(metadataOutput) {
session.addOutput(metadataOutput)
metadataOutput.metadataObjectTypes = supportedBarcodeTypes
metadataOutput.setMetadataObjectsDelegate(delegate, queue: DispatchQueue.main)
}
let previewLayer = AVCaptureVideoPreviewLayer(session: session)
uiView.backgroundColor = UIColor.gray
previewLayer.videoGravity = .resizeAspectFill
uiView.layer.addSublayer(previewLayer)
uiView.previewLayer = previewLayer
session.startRunning()
}
}
}
func makeUIView(context: UIViewRepresentableContext<QrCodeScannerView>) -> QrCodeScannerView.UIViewType {
let cameraView = CameraPreview(session: session)
checkCameraAuthorizationStatus(cameraView)
return cameraView
}
static func dismantleUIView(_ uiView: CameraPreview, coordinator: ()) {
uiView.session.stopRunning()
}
private func checkCameraAuthorizationStatus(_ uiView: CameraPreview) {
let cameraAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: .video)
if cameraAuthorizationStatus == .authorized {
setupCamera(uiView)
} else {
AVCaptureDevice.requestAccess(for: .video) { granted in
DispatchQueue.main.sync {
if granted {
self.setupCamera(uiView)
}
}
}
}
}
func updateUIView(_ uiView: CameraPreview, context: UIViewRepresentableContext<QrCodeScannerView>) {
uiView.setContentHuggingPriority(.defaultHigh, for: .vertical)
uiView.setContentHuggingPriority(.defaultLow, for: .horizontal)
}
}
画面関連のクラス
最初に表示する画面
import SwiftUI
struct ContentView: View {
@ObservedObject var viewModel = ScannerViewModel()
var body: some View {
VStack {
Text("QR Code Reader")
.padding()
// 読み取ったQRコード表示位置
Text("URL = [ " + viewModel.lastQrCode + " ]")
Button(action: {
viewModel.isShowing = true
}){
Text("カメラ起動")
Image(systemName: "camera")
}
.fullScreenCover(isPresented: $viewModel.isShowing) {
SecondView(viewModel: viewModel)
}
}
}
}
2画面目
QRコード読み取りが成功したら最初の画面に戻ります。
import SwiftUI
struct SecondView: View {
@ObservedObject var viewModel : ScannerViewModel
var body: some View {
Text("SecondView")
ZStack {
// QRコード読み取りView
QrCodeScannerView()
.found(r: self.viewModel.onFoundQrCode)
.interval(delay: self.viewModel.scanInterval)
VStack {
VStack {
Text("Keep scanning for QR-codes")
.font(.subheadline)
Text("QRコード読み取り結果 = [ " + self.viewModel.lastQrCode + " ]")
.bold()
.lineLimit(5)
.padding()
Button("Close") {
self.viewModel.isShowing = false
}
}
.padding(.vertical, 20)
Spacer()
}.padding()
}
}
}
info.plist
カメラ使用許可設定を忘れずに
これを設定しないと、カメラ起動時にアプリが落ちます。
<key>NSCameraUsageDescription</key>
<string>QRCodeを読み取る為に使用します。</string>
最後に
アドバイス、ご指摘などコメント頂けるとありがたいです。