3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

SwiftUIでQRスキャンする

Posted at

概要

SwftUIでQRコードをスキャンするときのメモです。

環境

Xcode 14.2

実装手順

  1. カメラを起動するUIViewを実装する
  2. 実装したUIViewをSiwftUIで扱えるようにする
  3. カメラから受け取ったアウトプットをViewModelでハンドリングする

カメラを起動するUIViewを実装

セッションに関する設定をします。

UICameraView.swift
import THLogger
import UIKit
import AVFoundation

class UICameraView: UIView {
    private let metadataOutput = AVCaptureMetadataOutput()
    private var previewLayer: AVCaptureVideoPreviewLayer?
    private var captureSession: AVCaptureSession = .init()
    private var videoDevice: AVCaptureDevice? = .default(for: .video)
    
    weak var delegate: UICameraViewDelegate?
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        do {
            try configurePreviewLayer()
        } catch {
            THLogger.error(error)
        }
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        previewLayer?.frame = bounds
    }
}

// MARK: - CONFIGURE
extension UICameraView {
    private func configurePreviewLayer() throws {
        guard let videoDevice = videoDevice else { return }
        
        let input = try AVCaptureDeviceInput(device: videoDevice)
        captureSession.addInput(input)
        
        if captureSession.canAddOutput(metadataOutput) {
            captureSession.addOutput(metadataOutput)
            metadataOutput.metadataObjectTypes = [.qr]
            metadataOutput.setMetadataObjectsDelegate(self, queue: .main)
        }
        
        captureSession.beginConfiguration()
        if captureSession.canSetSessionPreset(.photo) {
            captureSession.sessionPreset = .photo
        }
        captureSession.commitConfiguration()
        
        DispatchQueue.global().async {
            self.captureSession.startRunning()
        }
        
        previewLayer = .init(session: captureSession)
        previewLayer?.videoGravity = .resizeAspectFill
        layer.addSublayer(previewLayer!)
    }
}

// MARK: - AVCaptureMetadataOutputObjectsDelegate
extension UICameraView: AVCaptureMetadataOutputObjectsDelegate {
    func metadataOutput(
        _ output: AVCaptureMetadataOutput,
        didOutput metadataObjects: [AVMetadataObject],
        from connection: AVCaptureConnection
    ) {
        delegate?.didOutput(output, metadataObjects: metadataObjects)
    }
}

UIViewRepresentable

UICameraViewUIViewRepresentableUICameraViewDelegateに準拠させます。
その後、PassthroughSubjectを使ってdelegateのデータを親クラスに渡しています。

CameraViewRepresentable.swift
import Combine
import AVFoundation
import SwiftUI

struct DidOutputObjects {
    let output: AVCaptureMetadataOutput
    let metadataObjects: [AVMetadataObject]
}

struct CameraViewRepresentable: UIViewRepresentable {
    private let didOutputSubject: PassthroughSubject<DidOutputObjects, Never>
    
    init(didOutputSubject: PassthroughSubject<DidOutputObjects, Never>) {
        self.didOutputSubject = didOutputSubject
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    func makeUIView(context: Context) -> UICameraView {
        let view = UICameraView()
        view.delegate = context.coordinator
        return view
    }
    
    func updateUIView(_ uiView: UICameraView, context: Context) {
    }
    
    class Coordinator: NSObject, UICameraViewDelegate {
        
        let parent: CameraViewRepresentable
        
        init(_ parent: CameraViewRepresentable) {
            self.parent = parent
        }
        
        func didOutput(_ output: AVCaptureMetadataOutput, metadataObjects: [AVMetadataObject]) {
            parent.didOutputSubject.send(.init(output: output, metadataObjects: metadataObjects))
        }
    }
}

ViewModel

PassthroughSubjectでQRコードのデータが渡ってくるので、以下のようにして受け取り画面に表示します。

ScannerViewModel.swift
extension ScannerViewModel {
    private func subscribeDidOutput() {
        didOutputSubject
            .receive(on: DispatchQueue.main)
            .sink { [weak self] objects in
                guard let self = self else { return }
                guard let metadataObject = objects.metadataObjects.first else { return }
                guard let readable = metadataObject as? AVMetadataMachineReadableCodeObject else { return }
                guard let stringValue = readable.stringValue else { return }
                self.lastQrCode = stringValue
            }
            .store(in: &cancellables)
    }
}

全体のソースコードはこちらに載せております。

QRコードを読み取り終わり、画面は以下のようになりました。

参考記事

https://qiita.com/ikaasamay/items/58d1a401e98673a96fd2
https://www.hackingwithswift.com/books/ios-swiftui/scanning-qr-codes-with-swiftui
https://blog.devgenius.io/camera-preview-and-a-qr-code-scanner-in-swiftui-48b111155c66

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?