Implementing QR Code Scanning via Camera in Mobile Applications
DataScannerViewController in iOS 16+ made QR scanner implementation a trivial task — approximately 20 lines of code. On Android, ML Kit handles it similarly. The main work is UX around the scanner: lighting, animation, camera access error handling.
iOS: Two Approaches
DataScannerViewController (iOS 16+)
guard DataScannerViewController.isSupported,
DataScannerViewController.isAvailable else { return }
let scanner = DataScannerViewController(
recognizedDataTypes: [.barcode(symbologies: [.qr])],
qualityLevel: .balanced,
recognizesMultipleItems: false,
isHighlightingEnabled: true
)
scanner.delegate = self
try? scanner.startScanning()
present(scanner, animated: true)
recognizesMultipleItems: false — stops at the first code found. isHighlightingEnabled draws a frame around the code automatically.
Delegate:
func dataScanner(_ dataScanner: DataScannerViewController,
didAdd addedItems: [RecognizedItem],
allItems: [RecognizedItem]) {
guard case let .barcode(barcode) = addedItems.first,
let payload = barcode.payloadStringValue else { return }
dataScanner.stopScanning()
handleQR(payload)
}
AVFoundation (iOS 14-15)
For iOS 14-15 support — AVCaptureMetadataOutput with metadataObjectTypes = [.qr]. Detailed implementation is the same as for general barcode scanning.
Android: ML Kit in 10 Lines
val options = BarcodeScannerOptions.Builder()
.setBarcodeFormats(Barcode.FORMAT_QR_CODE)
.build()
val scanner = BarcodeScanning.getClient(options)
Through CameraX ImageAnalysis — same approach as general barcode scanning. Specifying the exact format FORMAT_QR_CODE speeds up recognition roughly twofold compared to FORMAT_ALL_FORMATS.
What Really Takes Time: UI Around the Scanner
Scanning overlay with reticle. Standard pattern — semi-transparent overlay with transparent rectangle in center. On iOS: CAShapeLayer with evenOdd fill rule or SwiftUI Canvas. On Android: custom View with PorterDuff.Mode.CLEAR.
Scanning line animation. Users expect a moving line — without it, unclear if process is active. Simple CABasicAnimation on iOS or ObjectAnimator on Android.
Flashlight. Torch toggle: on iOS AVCaptureDevice.torchMode = .on, on Android CameraControl.enableTorch(true).
Camera permission. If user declined the request, standard flow: AVCaptureDevice.authorizationStatus(for: .video) == .denied → "Open Settings" button → UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!). On Android — ActivityCompat.shouldShowRequestPermissionRationale() for explanation.
Implementation timeline: 1 day (basic function) — 2 days (with custom UI and handling all edge cases). Cost calculated individually.







