引言
随着移动互联网的发展,二维码已经成为信息传递和身份验证的重要工具。从扫码支付到产品追溯,二维码的应用场景无处不在。在 iOS 开发中,利用 AVFoundation 框架进行二维码扫描是一项常见的需求。AVFoundation 提供了强大的媒体捕获和处理能力,支持从摄像头采集视频流并进行实时数据解析。
本篇博客将深入探讨如何使用 AVFoundation 框架实现二维码扫描功能。我们将简要回顾视频采集的基本流程,重点介绍如何配置摄像头会话、设置二维码扫描输出,并处理二维码数据中的坐标与旋转信息。此外,我们还将讨论如何优化二维码数据的展示效果,确保二维码扫描的准确性与流畅度。
会话的设置与输入
在前面的拍照以及录制视频的博客中, 我们已经介绍了会话的配置与设置输入和输出的完整流程,在二维码扫描的部分整体流程还是没有太大的变化:
- 设置AVCaptureSession会话。
- 获取输入设备。
- 添加会话输入。
- 添加会话输出。
- 启动会话。
- 停止会话。
其中除了添加会话输出以外代码并没有区别:
import UIKit
import AVFoundation
class PHCaptureQRController: NSObject {
/// 会话
let session = AVCaptureSession()
/// 输出
private let metadataOutput = AVCaptureMetadataOutput()
/// 输入
private var captureDeviceInput: AVCaptureDeviceInput?
/// 队列
private let sessionQueue = DispatchQueue(label: "com.example.captureSession")
/// 代理
weak var delegate: PHCaptureProtocol?
/// 配置会话
func setupConfigureSession() {
session.beginConfiguration()
// 1.设置会话预设
setupSessionPreset()
// 2.设置会话输入
if !setupSessionInput(device: self.getDefaultCameraDevice()) {
delegate?.captureError(NSError(domain: "PHCaptureQRController", code: 1001, userInfo: [NSLocalizedDescriptionKey: "Failed to add input"]))
return
}
// 3.设置会话输出
if !setupSessionOutput() {
delegate?.captureError(NSError(domain: "PHCaptureQRController", code: 1002, userInfo: [NSLocalizedDescriptionKey: "Failed to add output"]))
return
}
session.commitConfiguration()
}
/// 设置会话话预设
private func setupSessionPreset() {
session.sessionPreset = .photo
}
/// 设置会话输入
private func setupSessionInput(device: AVCaptureDevice? = nil) -> Bool {
// 1.获取摄像头
guard let device = device else { return false }
do {
captureDeviceInput = try AVCaptureDeviceInput(device: device)
if session.canAddInput(captureDeviceInput!) {
session.addInput(captureDeviceInput!)
} else {
return false
}
} catch {
delegate?.captureError(error)
return false
}
return true
}
/// 设置会话输出
private func setupSessionOutput() -> Bool {
return true
}
/// 启动会话
func startSession() {
sessionQueue.async {
if !self.session.isRunning {
self.session.startRunning()
}
}
}
/// 停止会话
func stopSession() {
sessionQueue.async {
if self.session.isRunning {
self.session.stopRunning()
}
}
}
/// 获取默认摄像头
private func getDefaultCameraDevice() -> AVCaptureDevice? {
return getCameraDevice(position: .back)
}
/// 获取指定摄像头
private func getCameraDevice(position: AVCaptureDevice.Position) -> AVCaptureDevice? {
let devices = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: position).devices
return devices.first
}
}
会话输出及元数据详解
接下来我们将重点关注二维码扫描的输出部分,包括 AVCaptureMetadataOutput 的配置以及如何处理扫描到的二维码数据。
设置会话输出
首先,我们创建一个 AVCaptureMetadataOutput 实例,并将其添加到会话中。这将允许会话处理元数据输出,并将扫描结果传递给代理方法。
/// 输出
private let metadataOutput = AVCaptureMetadataOutput()
/// 设置会话输出
private func setupSessionOutput() -> Bool {
if session.canAddOutput(metadataOutput) {
session.addOutput(metadataOutput)
// 配置元数据类型 为二维码
metadataOutput.metadataObjectTypes = [.qr]
// 设置 代理及输出队列
metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
} else {
return false
}
return true
}
有时候我们希望仅扫描屏幕中的一部分区域,避免扫描到远处或其他不相关的二维码。为此,我们可以通过 rectOfInterest 属性来限定扫描的区域。rectOfInterest 使用相机坐标系,并将矩形区域映射到摄像头画面中的指定位置。
例如,如果我们希望只扫描屏幕中心的一部分区域:
let interestRect = CGRect(x: 0.3, y: 0.3, width: 0.4, height: 0.4)
metadataOutput.rectOfInterest = previewLayer.metadataOutputRectOfInterest(for: interestRect)
AVCaptureVideoPreviewLayer 为我们提供了一个摄像头坐标与屏幕坐标转换的方法。
处理二维码扫描结果
当摄像头捕获到二维码时,AVCaptureMetadataOutput 会将二维码的相关信息传递给代理方法。在我们的代理方法中,二维码数据包含了二维码的位置、旋转角度、以及二维码的内容等信息。
我们需要实现 AVCaptureMetadataOutputObjectsDelegate 协议,特别是 metadataOutput(_:didOutputMetadataObjects:from:) 方法,该方法会在扫描到二维码时被调用。
我们将元数据通过代理直接回调到视图控制器。
//MARK: - AVCaptureMetadataOutputObjectsDelegate
func metadataOutput(_ output: AVCaptureMetadataOutput,
didOutput metadataObjects: [AVMetadataObject],
from connection: AVCaptureConnection) {
delegate?.captureQRCode(metadataObjects)
}
在视图控制器中,我们通过实现代理方法来集中处理所有扫描到的二维码信息。该方法会:
- 遍历所有扫描到的二维码对象;
- 提取二维码内容;
- 将二维码的区域转换为屏幕坐标系;
- 为每个二维码创建一个高亮图层,标记出其在画面中的位置;
- 移除已经不再出现的二维码图层。
以下是我们用于处理扫描结果的核心方法:
func captureQRCode(_ codes: [Any]) {
guard let previewLayer = self.previewView.layer as? AVCaptureVideoPreviewLayer else { return }
// 初始化要移除的二维码(本次未识别到的)
var lostCodes: [String] = self.codeLayerMap.keys.map { $0 }
for code in codes {
if let readableObject = code as? AVMetadataMachineReadableCodeObject,
let stringValue = readableObject.stringValue {
// 移除本次扫描到的二维码
lostCodes.removeAll(where: { $0 == stringValue })
// 获取或创建图层
var boundsLayer = self.codeLayerMap[stringValue]
if boundsLayer == nil {
boundsLayer = self.buildBoundsLayer()
self.codeLayerMap[stringValue] = boundsLayer
previewView.layer.addSublayer(boundsLayer!)
}
// 更新二维码的边框位置
if let transformed = previewLayer.transformedMetadataObject(for: readableObject),
let bounds = transformed.bounds {
boundsLayer?.path = self.buildBezierPath(bounds: bounds).cgPath
}
// 打印二维码内容(此处可扩展业务逻辑)
print("扫描到二维码内容:\(stringValue)")
}
}
// 移除已丢失的二维码图层
for lost in lostCodes {
if let layer = self.codeLayerMap[lost] {
layer.removeFromSuperlayer()
self.codeLayerMap.removeValue(forKey: lost)
}
}
}
而其中的buildBoundsLayer()方法与buildBezierPath()方法,是用来构建一个图层来描绘出二维码的位置及大小,代码实现如下:
private func buildBoundsLayer() -> CAShapeLayer {
let boundsLayer = CAShapeLayer()
boundsLayer.fillColor = UIColor.clear.cgColor
boundsLayer.strokeColor = UIColor.red.cgColor
boundsLayer.lineWidth = 4.0
return boundsLayer
}
private func buildBezierPath(bounds: CGRect) -> UIBezierPath {
return UIBezierPath(rect: bounds)
}
最终实现效果如下:
可以看见摄像头已经成功识别的二维码,并且根据识别到的二维码元数据信息沿着二维码的周围绘制了一个红色的矩形边框。
结语
通过 AVFoundation 提供的强大接口,我们实现了从摄像头获取图像数据、检测二维码、并在界面中高亮显示二维码区域的完整流程。借助 AVCaptureMetadataOutput,我们不仅可以获取二维码的内容,还可以准确绘制其在预览画面中的位置,为用户提供清晰直观的视觉反馈。
更重要的是,这种解耦的设计也为后续功能扩展提供了便利——你可以:
- 添加二维码类型过滤(如限制为 URL 或特定格式);
- 使用四角坐标绘制更准确的多边形边框;
- 加入识别成功后的提示音或震动反馈;
- 自动跳转到二维码链接页面,或对内容进行分类处理;
- 配合视野限定、对焦优化,提升扫码效率。
二维码扫描只是 AVFoundation 视频采集能力的一个应用点。掌握了这一流程后,你也可以尝试拓展到身份证识别、条形码扫描、图像定位等更多高级视觉处理任务。