Capture machine-readable codes and text with VisionKit
Spatial Computing 进阶 20m

VisionKit DataScanner:一行代码搞定实时扫码和文字识别

Capture machine-readable codes and text with VisionKit

2022年6月6日

在 Apple 官方观看视频

一句话判断

DataScannerViewController 是 Apple 对「我只需要扫个码/识别下文字,为什么要把 AVFoundation + Vision 搞这么复杂」这个问题的终极回答——一个 UIViewController 子类,带预览、带引导、带高亮,present 就能用。

这场 Session 讲了什么

iOS 上做实时数据扫描,之前的方案要么用 AVFoundation 的 metadata output(只支持机器码),要么把 AVFoundation 和 Vision 组合起来(代码量大,坐标转换麻烦)。iOS 16 的 DataScannerViewController 把这些全部封装成一个开箱即用的 ViewController。

你告诉它要识别什么类型的数据(QR 码、条形码、文字,或它们的组合),然后 present 并调用 startScanning()。它自动提供实时相机预览、引导标签、识别项高亮、点击对焦和双指缩放。坐标全部是 view 坐标系,不用再做 image space 到 Vision 坐标到 view 坐标的痛苦转换。

你还可以指定 region of interest(感兴趣区域)来限制扫描范围,用 view 坐标设置。文字识别支持指定 content type(URL、电话号码等七种类型)和语言提示(iOS 16 新增日语和韩语)。机器码识别支持 Vision 支持的所有码制。

值得深挖的点

三种质量级别的取舍。 qualityLevel 有 fast、balanced、accurate 三种。Fast 牺牲分辨率换取速度,适合识别大号文字和近距离码。Accurate 用最高精度,适合微小 QR 码和细小序列号。Balanced 是默认值,Session 推荐大多数场景先用 balanced,效果不好再切换。我觉得这个设计比让你自己调 resolution 要友好得多。

delegate 和 AsyncStream 两种数据消费方式。 如果你需要自定义高亮(比如给每个识别到的条形码画一个彩色边框),用 delegate 的 didAdd/didUpdate/didRemove 三个方法来管理高亮 view 的生命周期。如果你不需要自定义 UI,用 recognizedItems 这个 AsyncStream 更简洁——它持续更新当前识别到的所有项目。两种方式拿到的数据都按自然阅读顺序排列(对文字识别特别有用)。

代码片段

创建并启动 DataScanner:

import VisionKit

// 指定要识别的数据类型
let types: Set<DataScannerViewController.RecognizedDataType> = [
    .barcode(symbologies: [.qr, .ean13]),
    .text(textContentType: .URL)  // 只识别 URL 类型的文字
]

// 创建并配置 scanner
let scanner = DataScannerViewController(
    recognizedDataTypes: types,
    qualityLevel: .balanced,
    recognizesMultipleItems: true,     // 同时识别多个项目
    isHighFrameRateTrackingEnabled: true, // 高帧率追踪,让高亮更跟手
    isPinchToZoomEnabled: true,
    isGuidanceEnabled: true,           // 顶部显示引导标签
    isHighlightingEnabled: true        // 系统自带高亮
)

present(scanner, animated: true) {
    try? scanner.startScanning()
}

处理用户点击识别到的项目:

// RecognizedItem 是一个 enum,关联 text 或 barcode
func dataScanner(_ dataScanner: DataScannerViewController, 
                 didTapOn item: RecognizedItem) {
    switch item {
    case .text(let text):
        // text.transcript 是识别到的文字
        // 文字精度会随扫描时间推移而提高
        handleRecognizedText(text.transcript)
    case .barcode(let barcode):
        // barcode.payloadStringValue 是条码内容
        if let payload = barcode.payloadStringValue {
            openURL(from: payload)
        }
    @unknown default:
        break
    }
}

自定义高亮的完整 delegate 实现:

var highlights: [RecognizedItem.ID: HighlightView] = [:]

// 新项目出现 → 创建高亮
func dataScanner(_ dataScanner: DataScannerViewController, 
                 didAdd addedItems: [RecognizedItem], allItems: [RecognizedItem]) {
    for item in addedItems {
        let highlight = HighlightView(for: item)
        highlights[item.id] = highlight
        // 添加到 overlayContainerView,确保在预览之上
        dataScanner.overlayContainerView.addSubview(highlight)
    }
}

// 项目移动 → 更新高亮位置
func dataScanner(_ dataScanner: DataScannerViewController,
                 didUpdate updatedItems: [RecognizedItem], allItems: [RecognizedItem]) {
    for item in updatedItems {
        highlights[item.id]?.animate(to: item.bounds)
    }
}

// 项目消失 → 移除高亮
func dataScanner(_ dataScanner: DataScannerViewController,
                 didRemove removedItems: [RecognizedItem], allItems: [RecognizedItem]) {
    for item in removedItems {
        highlights.removeValue(forKey: item.id)?.removeFromSuperview()
    }
}

最佳实践

  • 使用 isSupported 检查设备是否支持(2018 及之后有 Neural Engine 的 iPhone/iPad),不支持时隐藏相关 UI 入口。
  • 使用 isAvailable 检查用户是否授权了摄像头访问,以及设备是否有摄像头限制。
  • 如果你能预判语言环境,传 languages 参数给文字识别,尤其在两种语言看起来相似时很有帮助。
  • overlayContainerView 添加自定义高亮,不要添加到 scanner 的其他 view 上。
  • 每个项目的 bounds 是四个角点(不是矩形),因为透视变换下文字可能是梯形。

还有什么值得关注

  • capturePhoto() 可以在扫描的同时拍摄高质量静态照片。
  • iOS 16 的 Live Text 新增日语和韩语支持,用 supportedTextRecognitionLanguages 获取完整列表。
  • DataScanner 返回的文字项目按自然阅读顺序排列,第一个元素就是用户最先读到的。
WWDC 2022