アプリ開発始めた方、カメラアプリを何となく使っているアプリ開発者向け、カメラアクセス承認チェックが実装されていない古いアプリに触る機会を得たタイミングで得られた知見についてのメモ。
端的に云うと
iOS カメラ機能を使う場合コードリーダー機能とカメラ機能でエラーが出るタイミングが異なるので問題切り分け時に注意する。
1) iOSアプリの花形、カメラアプリ
最初にiOSアプリを作る、試しに作ってみるとしたらハードウェアを使って見栄えがするカメラアプリを選択選びたくなるのが開発者の心情(?)である。Webに掲載されている記事としてもカメラアプリを作るのは定番といえる。
2) 併用されるカメラ機能
モバイルデバイスのカメラは、被写体を撮影録画する機能のほか、QRコードなどコードリーダー機能を介してモバイルデバイスに情報を取り込み機能の側面も持っている。カメラのファインダーの役割を果たすプレビュー機能を合わせると、
- 被写体の撮影録画
- コードスキャン
- プレビューの表示
がモバルカメラが提供している一般に考えつく機能だろう。3のプレビューは常に使うとして1と2は個別に使う場合もあるが、アプリ内で個別の機能として使用する、それぞれを組み合わせて使用するなどが考えられる。商品の外観を撮影し、商品のバーコードを読み取ることができるメタ情報を紐づけるような機能は撮影機能とコードスキャンが組み合わされた例といえる。
撮影機能とコードスキャン機能が1つのアプリに含まれている事はそれほど特殊というわけではない。
3) アクセス承認未対応アプリでのクラッシュ挙動の違い
カメラアクセス非承認時を考慮しない古いアプリをメンテナンス機会を得た際に気付いた点として、アプリがクラッシュするタイミングが異なってくる。
カメラアクセス承認についてはこちらの記事が参考になる
【iOS】アプリに各アクセス権限が付与されているか確認する方法 - Qiita
iOSのカメラ機能を使った写真撮影/コードリーダー(ここではQRCode)のシンプルなサンプルコードは以下となる。
GitHub - notoroid/SimpleCamera
import UIKit
import AVFoundation
class SimpleCameraViewController: UIViewController {
@IBOutlet weak var previewPlaceholderView: UIView!
@IBOutlet weak var captureButton: UIButton!
@IBOutlet weak var modeLabel: UILabel!
let session = AVCaptureSession()
#if QRCODE_READER
let metadataOutput = AVCaptureMetadataOutput()
#else
let output = AVCapturePhotoOutput()
#endif
let settings = AVCapturePhotoSettings()
var previewLayer: AVCaptureVideoPreviewLayer? = nil
override func viewDidLoad() {
super.viewDidLoad()
#if QRCODE_READER
setupReaderFeature()
#else
setupCamFeature()
#endif
}
#if QRCODE_READER
private func setupReaderFeature() {
modeLabel.text = "QRCode Reader"
captureButton.isHidden = true
guard let captureDevice = AVCaptureDevice.default(for: .video), let input = try? AVCaptureDeviceInput(device: captureDevice) , session.canAddInput(input), session.canAddOutput(metadataOutput) else {
return
}
session.addInput(input)
session.addOutput(metadataOutput)
session.startRunning()
metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
metadataOutput.metadataObjectTypes = [.qr]
previewLayer = AVCaptureVideoPreviewLayer(session: session)
if let previewLayer = previewLayer {
previewLayer.frame = previewPlaceholderView.bounds
previewLayer.videoGravity = .resizeAspectFill
previewPlaceholderView.layer.addSublayer(previewLayer)
}
previewPlaceholderView.clipsToBounds = true
}
#else
private func setupCamFeature() {
modeLabel.text = "Camera"
guard let captureDevice = AVCaptureDevice.default(for: .video), let input = try? AVCaptureDeviceInput(device: captureDevice), session.canAddInput(input), session.canAddOutput(output) else {
return
}
session.addInput(input)
session.addOutput(output)
session.startRunning()
previewLayer = AVCaptureVideoPreviewLayer(session: session)
if let previewLayer = previewLayer {
previewLayer.frame = previewPlaceholderView.bounds
previewLayer.videoGravity = .resizeAspectFill
previewPlaceholderView.layer.addSublayer(previewLayer)
}
previewPlaceholderView.clipsToBounds = true
}
@IBAction func onCapture(_ sender: Any) {
output.capturePhoto(with: AVCapturePhotoSettings(), delegate: self)
}
#endif
}
#if QRCODE_READER
extension SimpleCameraViewController: AVCaptureMetadataOutputObjectsDelegate {
func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
guard !metadataObjects.isEmpty, let metadataObject = metadataObjects.first, let qr = metadataObject as? AVMetadataMachineReadableCodeObject else {
return
}
print("qr=\(qr.stringValue)")
}
}
#else
extension SimpleCameraViewController: AVCapturePhotoCaptureDelegate {
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
guard let data = photo.fileDataRepresentation(), let image = UIImage(data: data) else {
return
}
print("width=\(image.size.width)")
print("height=\(image.size.height)")
}
}
#endif
プレビュー表示を担当するAVCaptureVideoPreviewLayer は、AVFoundationフレームワークが提供するUIKit向けのプレビュー用Layer派生オブジェクトでカメラのプレビュー映像を画面に最適化して表示する。カメラアクセス非承認時にAVCaptureVideoPreviewLayerを使ったプレビューはアプリはクラッシュせずプレビューだけが表示されない。
カメラアクセス非承認時にカメラ撮影は撮影決定時にアプリがクラッシュする。
コードリーダーはカメラ開始直後にクラッシュする。
これらの違いはカメラから入力された映像を出力されるoutputを使用するタイミングが撮影とコードリーダーでタイミングが異なることによる違いと思われる。撮影は決定時までoutputからの情報を必要とせずコードリーダーは起動直後からoutputからの情報を必要とするためアプリクラッシュの発生タイミングが異なってくる。
カメラアクセス承認未対応の場合のクラッシュタイミング
機能名 | クラッシュタイミング | 該当オブジェクト |
---|---|---|
カメラ撮影 | 撮影タイミング | AVCapturePhotoOutput |
コードリーダー | リーダー機能初期化時 | AVCaptureMetadataOutput |
プレビュー | クラッシュせず | AVCaptureVideoPreviewLayer |
4) まとめ
Appleのプライバシー重視の方向によりカメラからのプライバシーが流出することを避けるにはアプリを強制終了するのが最適解と思われているところがある。
サンプルコードとしてカメラアクセス承認に対応していない静止画撮影機能とコードリーダー機能のサンプルを確認してみて欲しい。実機のみ確認可能で、Xcode左上のスキーマをSimpleCamera/SimpleReader を切り替えて、カメラ撮影/カードリーダーを切り替えて違いを確認してみて欲しい。