
【Swift】MLKitを使用してQRコードを読み取ってみた
この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
DevelopersIOの自分の著者ページURLをQRコードにして、それを読み取った後にページを表示させたくなったのでQRコード読み取りについて調べてみました。
作ったもの

環境
- Xcode 13.2.1
処理の流れ
ざっくりの説明になりますが、
- カメラでビデオ撮影
- ビデオ撮影したものからフレームをキャプチャし、イメージを切り出す
- 切り出したイメージ内にQRコードがある場合は、そのQRコードからURLを取得する
- URLが取得出来たら、SafariViewControllerで当該ページを表示する
といった流れになっています。
今回はQRコード読み取りにMLKitを使用するのでインストールします。
MLKitのインストール
バーコードスキャンAPIは元々Firebaseの一部でありましたが、現在はFirebaseの有無に関わらず使用できるMLKit SDKに移行されております。 なので、今回はMLKit SDKを使用していきたいと思います。
こちらを見ると、まだSPMの対応がされてなさそうだったので今回はCocoaPodsを使用します。
下記をPod Fileに追記し、pod installを行います。
pod 'GoogleMLKit/BarcodeScanning', '2.6.0'
pod install完了後に.xcworkspaceが出来るのでそちらを開いて作業をしていきます。
Camera Usage Descriptionを設定
カメラを使用する為に、Info.plistにPrivacy - Camera Usage Descriptionを追加します。
カメラからの出力を取得する
まずはカメラからの出力をキャプチャする為のクラスVideoCaptureを作成します。
import AVFoundation
protocol VideoCaptureDelegate: AnyObject {
func videoCapture(_ videoCapture: VideoCapture, didSet previewLayer: AVCaptureVideoPreviewLayer)
func videoCapture(_ videoCapture: VideoCapture, didOutput sampleBuffer: CMSampleBuffer)
}
class VideoCapture: NSObject {
weak var delegate: VideoCaptureDelegate?
// AVの出入力のキャプチャを管理するセッションオブジェクト
private let captureSession = AVCaptureSession()
// ビデオを記録し、処理を行うためにビデオフレームへのアクセスを提供するoutput
private let videoOutput = AVCaptureVideoDataOutput()
// カメラセットアップとフレームキャプチャを処理する為のDispathQueue
private let sessionQueue = DispatchQueue(label: "video-capture-queue")
// キャプチャの開始
func startCapturing() {
// capture deviceのメディアタイプを決め、
// 何からインプットするかを決める
guard let captureDevice = AVCaptureDevice.default(for: .video),
let deviceInput = try? AVCaptureDeviceInput(device: captureDevice)
else { return }
// captureSessionにdeviceInputを入力値として入れる
captureSession.addInput(deviceInput)
// キャプチャセッションの開始
captureSession.startRunning()
// ビデオフレームの更新ごとに呼ばれるデリゲートをセット
videoOutput.setSampleBufferDelegate(self, queue: sessionQueue)
// captureSessionから出力を取得するためにdataOutputをセット
captureSession.addOutput(videoOutput)
// captureSessionをUIに描画するためにPreviewLayerにsessionを追加
let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
delegate?.videoCapture(self, didSet: previewLayer)
}
// キャプチャの再開
func restartCapturingIfNeeded() {
if !captureSession.isRunning {
captureSession.startRunning()
}
}
// キャプチャの停止
func stopCapturing() {
// キャプチャセッションの停止
captureSession.stopRunning()
}
}
extension VideoCapture: AVCaptureVideoDataOutputSampleBufferDelegate {
func captureOutput(_ output: AVCaptureOutput,
didOutput sampleBuffer: CMSampleBuffer,
from connection: AVCaptureConnection) {
delegate?.videoCapture(self, didOutput: sampleBuffer)
}
}
以下に詳細を説明します。
プロパティ
Delegate
weak var delegate: VideoCaptureDelegate?
AVCaptureSession
// AVの出入力のキャプチャを管理するセッションオブジェクト private let captureSession = AVCaptureSession()
AVCaptureVideoDataOutput()
// ビデオを記録し、処理を行うためにビデオフレームへのアクセスを提供するoutput private let videoOutput = AVCaptureVideoDataOutput()
SessionQueue
// カメラセットアップとフレームキャプチャを処理する為のDispathQueue private let sessionQueue = DispatchQueue(label: "video-capture-queue")
startCapturing()
キャプチャの開始にはstartCapturing()メソッドを使用します。
func startCapturing() {
// capture deviceのメディアタイプを決め、
// 何からインプットするかを決める
guard let captureDevice = AVCaptureDevice.default(for: .video),
let deviceInput = try? AVCaptureDeviceInput(device: captureDevice)
else { return }
// captureSessionにdeviceInputを入力値として入れる
captureSession.addInput(deviceInput)
// キャプチャセッションの開始
captureSession.startRunning()
// ビデオフレームの更新ごとに呼ばれるデリゲートをセット
videoOutput.setSampleBufferDelegate(self, queue: sessionQueue)
// captureSessionから出力を取得するためにdataOutputをセット
captureSession.addOutput(videoOutput)
// captureSessionをUIに描画するためにPreviewLayerにsessionを追加
let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
delegate?.videoCapture(self, didSet: previewLayer)
}
まず最初に、captureDeviceとして、AVCaptureDevice.default(for: .video)を設定しています。
AVCaptureDevice.default(for:)では様々なAVMediaTypeを指定できますが、今回はvideoからキャプチャしたので.videoにしています。
captrureDeviceからcaptureSessionへ取得データを提供する為に、AVCaptureDeviceInput(device: captureDevice)を準備しておきます。
addInput(_ input)
準備したdeviceInputをcaptureSessionに追加します。
startRunning()
インプットデータも決まったので、captureSession.startRunning()でcaptureSessionの実行をスタートさせます。
setSampleBufferDelegate(_:queue:)
カメラのビデオをキャプチャし、そのフレームが更新される度に画像の分類やViewへの描画を行いたいので、setSampleBufferDelegate(_:queue:)をセットしてデリゲートメソッドを使えるようにしておきます。
addOutput(_ output)
captureSession.addOutput(_ output)でcaptureSessionの出力にvideoOutputを追加します
AVCaptureVideoPreviewLayer
AVCaptureVideoPreviewLayerは、キャプチャされたビデオを表示するレイヤーで、今回はパラメーターにカメラからのキャプチャSessionとしてcaptureSessionを渡し、プレビューレイヤーをイニシャライズしています。
let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
previewLayerを定義したら、そのpreviewLayerをVideoCaptureDelegateメソッドのvideoCapture(_ videoCapture: VideoCapture, didSet previewLayer: AVCaptureVideoPreviewLayer)に渡しています。
キャプチャの再開
キャプチャを停止後に、再開する時に使用します。
func restartCapturingIfNeeded() {
if !captureSession.isRunning {
captureSession.startRunning()
}
}
キャプチャの停止
stopRunnningでcaptureSessionを停止します。
func stopCapturing() {
// キャプチャセッションの停止
captureSession.stopRunning()
}
AVCaptureVideoDataOutputSampleBufferDelegate
ビデオデータ出力からサンプルバッファを受信し、そのステータスを監視できるメソッドで、その中のcaptureOutput(_ output: , didOutput sampleBuffer: , from connection:)で新しいビデオフレームが書き込まれたことを検知することができます。
extension VideoCapture: AVCaptureVideoDataOutputSampleBufferDelegate {
func captureOutput(_ output: AVCaptureOutput,
didOutput sampleBuffer: CMSampleBuffer,
from connection: AVCaptureConnection) {
delegate?.videoCapture(self, didOutput: sampleBuffer)
}
}
このパラメーターCMSampleBufferには、ビデオフレームのデータやフレームに関する情報が含まれています。
そして、sampleBufferをVideoCaptureDelegateメソッドのvideoCapture(_ videoCapture: VideoCapture, didOutput sampleBuffer: CMSampleBuffer)に渡しています。
カメラ部分は完成したので、次はQRコードリーダー部分を作成します。
QRコードの読み取り
QRコードを読み取り、URLに変換するクラスQRCodeReaderを作成します。
import MLKit
import AVFoundation
protocol QRCodeReaderDelegate: AnyObject {
func qrCodeReader(_ qrCodeReader: QRCodeReader, didScan url: URL)
}
class QRCodeReader: NSObject {
weak var delegate: QRCodeReaderDelegate?
// バーコードスキャナー
private let barcodeScanner: BarcodeScanner = {
let barcodeOptions = BarcodeScannerOptions(formats: .qrCode)
let barcodeScanner = BarcodeScanner.barcodeScanner(options: barcodeOptions)
return barcodeScanner
}()
// サンプルバッファをスキャンする
func scan(sampleBuffer: CMSampleBuffer) {
// サンプルバッファをVisionImageに変換
let image = VisionImage(buffer: sampleBuffer)
// 画像の向きを指定
image.orientation = imageOrientation(
deviceOrientation: UIDevice.current.orientation,
cameraPosition: .back)
// バーコードスキャンの処理を開始
barcodeScanner.process(image) { [weak self] barcodes, error in
guard let self = self,
error == nil,
let barcodes = barcodes,
!barcodes.isEmpty else { return }
var scannedUrl: URL?
// バーコードの中にURL情報がある場合は取得する
for barcode in barcodes {
guard let urlString = barcode.url?.url else { continue }
scannedUrl = URL(string: urlString)
break
}
guard let url = scannedUrl else { return }
// URLが取得出来たのでDelegateメソッドにURLを渡す
self.delegate?.qrCodeReader(self, didScan: url)
}
}
// 画像データの向きを指定する
private func imageOrientation(deviceOrientation: UIDeviceOrientation,
cameraPosition: AVCaptureDevice.Position) -> UIImage.Orientation {
switch deviceOrientation {
case .portrait:
return cameraPosition == .front ? .leftMirrored : .right
case .landscapeLeft:
return cameraPosition == .front ? .downMirrored : .up
case .portraitUpsideDown:
return cameraPosition == .front ? .rightMirrored : .left
case .landscapeRight:
return cameraPosition == .front ? .upMirrored : .down
case .faceDown, .faceUp, .unknown:
return .up
@unknown default:
fatalError()
}
}
}
プロパティ
Delegate
weak var delegate: QRCodeReaderDelegate?
バーコードスキャナー
private let barcodeScanner: BarcodeScanner = {
let barcodeOptions = BarcodeScannerOptions(formats: .qrCode)
let barcodeScanner = BarcodeScanner.barcodeScanner(options: barcodeOptions)
return barcodeScanner
}()
今回はQRコードリーダーとして使用するので、バーコードスキャナーのオプションは、.qrCodeを指定していますが、他にも下記のフォーマットをサポートしています。
- code128
- code39
- code93
- codaBar
- dataMatrix
- EAN13
- EAN8
- ITF
- qrCode
- UPCA
- UPCE
- PDF417
- aztec
scan(sampleBuffer: CMSampleBuffer)
このscan(sampleBuffer:)でVideoCaptureが出力したsampleBufferからバーコード情報を取得します。
スキャン実装部分は、Google公式ドキュメントiOSでMLキットを使用してバーコードをスキャンするに丁寧に書いてありました。
func scan(sampleBuffer: CMSampleBuffer) {
// サンプルバッファをVisionImageに変換
let image = VisionImage(buffer: sampleBuffer)
// 画像の向きを指定
image.orientation = imageOrientation(
deviceOrientation: UIDevice.current.orientation,
cameraPosition: .back)
// バーコードスキャンの処理を開始
barcodeScanner.process(image) { [weak self] barcodes, error in
guard let self = self,
error == nil,
let barcodes = barcodes,
!barcodes.isEmpty else { return }
var scannedUrl: URL?
// バーコードの中からURL情報がある場合は取得する
for barcode in barcodes {
guard let urlString = barcode.url?.url else { continue }
scannedUrl = URL(string: urlString)
break
}
guard let url = scannedUrl else { return }
// URLが取得出来たのでDelegateメソッドにURLを渡す
self.delegate?.qrCodeReader(self, didScan: url)
}
}
まずは、サンプルバッファをVisionImageに変更して、画像の向きをimageOrientation(deviceOrientation:,cameraPosition:)で指定しています。
barcodeScanner.process(_ image:, completion:)
barcodeScanner.process(_ image:, completion:)に画像を渡して、その中にあるバーコードを情報を取得します。
エラーがない、またbarcodesがnilでない場合は、バーコードの中からURL取得を行ってます。今回は、URLしか取得していないですが、下記の例のようにbarcode.valueTypeをみて、タイプによって処理を変更することも可能です。
// 例
let valueType = barcode.valueType
switch valueType {
case .wiFi:
let ssid = barcode.wifi?.ssid
let password = barcode.wifi?.password
let encryptionType = barcode.wifi?.type
case .URL:
let title = barcode.url!.title
let url = barcode.url!.url
case .text
let text = barcode.displayValue
default:
print("process something")
}
バーコードからURLを取得出来たら、QRCodeReaderDelegateのqrCodeReader(_ qrCodeReader: QRCodeReader, didScan url: URL)に渡しています。
VideoCaptureとQRコード読み取りを組み合わせる
作成したクラスを使用して、キャプチャ出力からQRコード内のURLを取得し、その当該ページに遷移させます。
import UIKit
import AVFoundation
import SafariServices
class QRReadableCameraViewController: UIViewController {
@IBOutlet private weak var previewView: UIView!
private let videoCapture = VideoCapture()
private let qrCodeReader = QRCodeReader()
override func viewDidLoad() {
super.viewDidLoad()
qrCodeReader.delegate = self
videoCapture.delegate = self
videoCapture.startCapturing()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
videoCapture.restartCapturingIfNeeded()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
videoCapture.stopCapturing()
}
}
extension QRReadableCameraViewController: VideoCaptureDelegate {
// previewLayerがセットされた時に呼ばれる
func videoCapture(_ videoCapture: VideoCapture, didSet previewLayer: AVCaptureVideoPreviewLayer) {
previewView.layer.addSublayer(previewLayer)
previewLayer.frame = previewView.frame
}
// サンプルバッファーが出力される度に呼ばれる
func videoCapture(_ videoCapture: VideoCapture, didOutput sampleBuffer: CMSampleBuffer) {
qrCodeReader.scan(sampleBuffer: sampleBuffer)
}
}
extension QRReadableCameraViewController: QRCodeReaderDelegate {
// QRからURLがスキャンされた時に呼ばれる
func qrCodeReader(_ qrCodeReader: QRCodeReader, didScan url: URL) {
let safariViewController = SFSafariViewController(url: url)
present(safariViewController, animated: true)
}
}
プロパティ
@IBOutlet private weak var previewView: UIView! private let videoCapture = VideoCapture() private let qrCodeReader = QRCodeReader()
カメラの出力を画面に反映する為のpreviewView: UIViewをIBOutletで用意しておきます。また、用意した2つのクラスもインスタンス化します。
viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
qrCodeReader.delegate = self
videoCapture.delegate = self
videoCapture.startCapturing()
}
読み込み時には、各Delegateにselfを渡して、ビデオのキャプチャを開始しています。
viewWillAppear
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
videoCapture.restartCapturingIfNeeded()
}
遷移後の画面から戻ってきた際にキャプチャを再開できるようにrestartCapturingIfNeeded()を記述しています。
viewWillDisappear
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
videoCapture.stopCapturing()
}
画面が見えなくなる際は、キャプチャを停止しています。
VideoCaptureDelegate
didSet
func videoCapture(_ videoCapture: VideoCapture, didSet previewLayer: AVCaptureVideoPreviewLayer) {
previewView.layer.addSublayer(previewLayer)
previewLayer.frame = previewView.frame
}
previewLayerがセットされたら、画面反映用のpreviewViewのレイヤーとして追加し、frameをprevieView.frameと合わせています。
didOutput
func videoCapture(_ videoCapture: VideoCapture, didOutput sampleBuffer: CMSampleBuffer) {
qrCodeReader.scan(sampleBuffer: sampleBuffer)
}
VideoCaptureから出力としてサンプルバッファを受け取ったら、そのサンプルバッファをスキャンします。
QRCodeReaderDelegate
didScan
func qrCodeReader(_ qrCodeReader: QRCodeReader, didScan url: URL) {
let safariViewController = SFSafariViewController(url: url)
present(safariViewController, animated: true)
}
QRコードのスキャンでURLが取得したら、今回はそのURLを使ってSafariViewControllerを表示させています。
今回のQRコード
今回は、このURLhttps://dev.classmethod.jp/author/littleossa/をQRコードとして作成したので、クラスメソッドの私の著者ページが出ると完成です!

おわりに
MLKitを使用すると、本当に簡単にQRコードを読み取るアプリを作ることが出来ました。今回はQRコードでしたが、他にも認識できるものは沢山あるので他のものでも試したくなりました!SwiftUIでも試してみたいですね。
モバイルアプリ開発のチームメンバー絶賛募集中!
モバイル事業部では事業会社様と一緒に、数年間にわたり長期でモバイルアプリをグロースさせています。
そんなモバイルアプリ開発のチームメンバーを絶賛募集中です!
もちろんモバイルアプリ開発以外のエンジニアも募集中です!










