メルペイのQRコードスキャン機能(QRScanner)を、OSSにしました

Merpay Advent Calendar 2019 の12日目は、 Merpay iOS チームの @hitsu がお送りします。

目次

QRScannerとは

数行のソースコードでiOS13と同じようなQRコードスキャン機能を実装できるフレームワークです。

GitHub – mercari/QRScanner: A simple QR Code scanner framework for iOS. Provides a similar scan effect to ios13.

OSS開発の背景

日本ではMerpayなどのキャッシュレス決済アプリの普及に伴って、QRコードの利用率が上がっており、それに関連した開発も多くなっています。QRコードスキャン機能を実装するにあたり、Appleが提供している単純なAVFoundationフレームワークを使って、0からQRコードスキャン機能、アニメエフェクトなどを実装すると時間がかかります。さらに、カメラのアクセス許可、AVCaptureのDelegate非同期処理など、いろいろと注意すべき事が多くあります。そこでメルペイは、数行のソースコードで高効率と高品質なQRコードスキャン機能を実装できるように、QRScannerというフレームワークを作成して、OSSとして公開しました。

QRScannerの特徴

  • 高い利便性。数行のソースコードでQRコードスキャン機能を実装できます。
  • iOS13と同じようなアニメエフェクト。AppleがiOS13から提供しているQRコードアプリのアニメエフェクトと同等な機能を揃えています。
  • iOS 10+からサポートしており、iOS10でもiOS13と同じようなスキャン機能が使えます。
iOS 13.0で提供しているアプリ QRScannerで作ったサンプル
f:id:idhitsu:20191206174114g:plain f:id:idhitsu:20191206174201g:plain

使い方

基本な使い方

final class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let qrScannerView = QRScannerView(frame: view.bounds)
view.addSubview(qrScannerView)
qrScannerView.configure(delegate: self)
qrScannerView.startRunning()
}
}
extension ViewController: QRScannerViewDelegate {
func qrScannerView(_ qrScannerView: QRScannerView, didFailure error: QRScannerError) {
print(error)
}
func qrScannerView(_ qrScannerView: QRScannerView, didSuccess code: String) {
print(code)
}
}

カメラのアクセスを許可

カメラのアクセス許可が必要なので、Privacy - Camera Usage DescriptionをInfo.plistファイルに追加します。

f:id:idhitsu:20191204165021p:plain

QRScannerの導入方法

CocoaPodsを使う場合
  • Podfileファイルに下記を追加
  # platform :ios, '10.0'
pod 'MercariQRScanner'
  • 実行
  pod install
  • ソースコードにMercariQRScannerをImport
  import MercariQRScanner
Carthageを使う場合
  • Cartfileファイルに下記を追加
github "mercari/QRScanner"
  • 実行
> carthage update --platform iOS
  • Frameworksを追加
    • In Xcode, “Genera > Build Phase > Linked Frameworks and Library”
    • Add the framework to your project
    • Add a new run script and put the following code
/usr/local/bin/carthage copy-frameworks
  • “+”をクリックして、pathを追加
$(SRCROOT)/Carthage/Build/iOS/QRScanner.framework
  • ソースコードにQRScannerをImport
import QRScanner

カスタマイズ

カスタマイズできるもの:

  • focusImage:フォーカス画像、ディフォルト値は UIImage(named: “scan_qr_focus”)です。
  • focusImagePadding:フォーカス画像と取得したQRコードイメージの隙間、ディフォルト値は8.0です。
  • animationDuration:アニメーションが始まって終わるまでの時間、ディフォルト値は0.5秒です。
  • FlashButton:FlashButtonのサンプル

ソースコードでカスタマイズ

override func viewDidLoad() {
super.viewDidLoad()
let qrScannerView = QRScannerView(frame: view.bounds)
// カスタマイズ
qrScannerView.focusImage = UIImage(named: "scan_qr_focus")
qrScannerView.focusImagePadding = 8.0
qrScannerView.animationDuration = 0.5
qrScannerView.configure(delegate: self)
view.addSubview(qrScannerView)
qrScannerView.startRunning()
}

Interface Builderでカスタマイズ

Custom Classを設定 カスタマイズを設定
f:id:idhitsu:20191204180402p:plain f:id:idhitsu:20191204180214p:plain

フラッシュボタンを使いたい場合

final class ViewController: UIViewController {
...
@IBOutlet var flashButton: FlashButton!
@IBAction func tapFlashButton(_ sender: UIButton) {
qrScannerView.setTorchActive(isOn: !sender.isSelected)
}
}
extension ViewController: QRScannerViewDelegate {
...
func qrScannerView(_ qrScannerView: QRScannerView, didChangeTorchActive isOn: Bool) {
flashButton.isSelected = isOn
}
}

内部実装

f:id:idhitsu:20191205174012p:plain

主要な実行フロー

  • configure
    • 初期化
      • AVCaptureDevice、AVCaptureDeviceInput、AVCaptureMetadataOutput、AVCaptureVideoDataOutputを設定
      • 問題があれば、QRScannerError.DeviceErrorを返す
      • videoInput、metadataOutput、videoDataOutputを設定して、AVCaptureSessionにcommitする
      • TorchActiveを初期化
      • 初回のみ、カメラのアクセス許可のダイアログを表示
    • カメラのPreviewLayerを設定
    • フォーカス画像を初期化
  • startRunning
    • スキャンする前にAuthorizationStatusをチェック
    • 問題が無ければ、AVCaptureMetadataOutputObjectsDelegateへ依頼して、QRコードスキャンがスタートする
  • scan QR Code(AVCaptureMetadataOutputObjectsDelegate)
    • QRコードをスキャンして
    • 見つかったら、QRコードの文字列とQRコード四角の座標を返す
    • AVCaptureVideoDataOutputSampleBufferDelegateへ依頼して、画像の抽出がスタートする
  • extract QR Code image(AVCaptureVideoDataOutputSampleBufferDelegate)
    • フレーム毎にCMSampleBufferを取得して
    • CMSampleBufferをフル画面の画像へ変換して
    • フル画面の画像から、QRコード部分の画像を抽出する
  • show animation effect
    • AVCaptureMetadataOutputObjectsDelegateで取得したQRコード四角の座標利用して、フォーカス画像の遷移先の位置と回転角度などを計算して
    • フォーカス画像のアニメエフェクトを実行して
    • フォーカス画像の枠内に抽出したQRコード画像を表示

アニメエフェクトの実装

AVCaptureMetadataOutputObjectsDelegateでQRコードの四角の座標が取得できる。四角の座標を使って、アニメエフェクトを実装しています。

  • 1.サイズ変換:QRコードは四角形なので、四角の座標から四角形のすべての辺の長さを計算できます、フォーカス画像の辺の長さはQRコードの一番長い辺の長さへ変換しています。
        // 一番長い辺の長さを計算:
var maxSide: CGFloat =  hypot(corners[3].x - corners[0].x, corners[3].y - corners[0].y)
for (i, _) in corners.enumerated() {
if i == 3 { break }
let side = hypot(corners[i].x - corners[i+1].x, corners[i].y - corners[i+1].y)
maxSide = side > maxSide ? side : maxSide
}
maxSide += focusImagePadding * 2
  • 2.位置移動:四角の座標で、QRコードの位置を取得して、フォーカス画像はその位置に移動します。
        // QRコードの位置:
let path = UIBezierPath()
path.move(to: corners[0])
corners[1..<corners.count].forEach() {
path.addLine(to: $0)
}
path.close()
// QRコードの位置へ移動
strongSelf.focusImageView.frame = path.bounds
  • 3.回転:QRコードの四角形とフォーカス画像の四角形には角度の差があります、その角度差を計算して、フォーカス画像を回転させます。ちなみに、QRコードの形によって、時計回り(右回り)か反時計回り(左回り)へ回転する場合があります。

角度の差:

反時計回り(左回り)へ回転する場合 時計回り(右回り)へ回転する場合
f:id:idhitsu:20191206162656p:plain f:id:idhitsu:20191206162834p:plain
f:id:idhitsu:20191206180424g:plain f:id:idhitsu:20191206180526g:plain

逆三角関数の反正接を使って、回転角度を計算:

角度 θ
f:id:idhitsu:20191205181538p:plain f:id:idhitsu:20191205210106p:plain
        let aSide: CGFloat
let bSide: CGFloat
// 反時計回りの場合
if corners[0].x < corners[1].x {
aSide = corners[0].x - corners[1].x
bSide = corners[1].y - corners[0].y
// 時計回りの場合
} else {
aSide = corners[2].y - corners[1].y
bSide = corners[2].x - corners[1].x
}
let degrees = atan(aSide / bSide)

終わりに

メルペイのQRScannerというOSSを紹介しました。QRコードスキャン機能を実装する際には便利に使えると思うので、ぜひ活用してみてください。改善ポイントがあればPullRequestもお待ちしております。

github.com

メルペイではミッション・バリューに共感しているiOSエンジニアを募集しています。一緒に働ける仲間をお待ちしております。

apply.workable.com

明日のMerpay Advent Calendar 執筆担当は、Frontend チームの @tanakaworld さんです。引き続きお楽しみください。

※ QRコードは(株)デンソーウェーブの登録商標です