Merpay Advent Calendar 2019 の12日目は、 Merpay iOS チームの @hitsu がお送りします。
目次
QRScannerとは
数行のソースコードでiOS13と同じようなQRコードスキャン機能を実装できるフレームワークです。
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で作ったサンプル |
---|---|
使い方
基本な使い方
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ファイルに追加します。
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を設定 | カスタマイズを設定 |
---|---|
フラッシュボタンを使いたい場合
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 } }
内部実装
主要な実行フロー
- 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コードの形によって、時計回り(右回り)か反時計回り(左回り)へ回転する場合があります。
角度の差:
反時計回り(左回り)へ回転する場合 | 時計回り(右回り)へ回転する場合 |
---|---|
逆三角関数の反正接を使って、回転角度を計算:
角度 θ | 図 |
---|---|
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もお待ちしております。
メルペイではミッション・バリューに共感しているiOSエンジニアを募集しています。一緒に働ける仲間をお待ちしております。
明日のMerpay Advent Calendar 執筆担当は、Frontend チームの @tanakaworld さんです。引き続きお楽しみください。
※ QRコードは(株)デンソーウェーブの登録商標です