Highlight
iOS 上做实时数据扫描,之前的方案要么用 AVFoundation 的 metadata output(只支持机器码),要么把 AVFoundation 和 Vision 组合起来(代码量大,坐标转换麻烦)。iOS 16 的
DataScannerViewController把这些全部封装成一个开箱即用的 ViewController。
核心内容
做一个扫码界面,过去有两条路。
只识别二维码、条形码,可以用 AVFoundation,把输入、输出接到 capture session,再拿到 AVMetadataObject。如果还要识别文字,就要把 AVFoundation 和 Vision 接起来:相机输出 sample buffer,Vision 跑文字或条码识别请求,再把 observation 映射回界面。
麻烦点不在识别本身。麻烦点在实时相机预览、用户授权、点击对焦、双指缩放、识别项高亮,以及从 image space 到 Vision 坐标再到 view 坐标的转换。
DataScannerViewController 是 iOS 16 的新选择。它在 VisionKit 里,把 AVFoundation 和 Vision 封装成一个 UIViewController 子类。你指定要识别的类型,present 它,然后调用 startScanning()。相机预览、引导文字、系统高亮、点击对焦和缩放都由系统处理。
它适合的场景很直接:仓库拣货、收银、票据录入、现场盘点。这些 App 的主要工作是扫到数据之后的业务流程。Data Scanner 让扫描部分变短。
详细内容
先判断设备和权限
(04:01)Data Scanner 不是所有设备都支持。Apple 在演讲中说明,2018 年及之后、带 Apple Neural Engine 的 iPhone 和 iPad 支持它。
因此入口按钮不要永远显示。先用 isSupported 判断硬件支持,再用 isAvailable 判断当前是否可用。isAvailable 会受相机授权和设备限制影响,例如 Screen Time 里的相机访问限制。
import VisionKit
if DataScannerViewController.isSupported,
DataScannerViewController.isAvailable {
showScanButton()
} else {
hideScanButton()
}
关键点:
import VisionKit引入DataScannerViewController。isSupported用来判断设备硬件是否支持实时数据扫描。isAvailable用来判断当前状态是否允许扫描,包括相机权限和系统限制。- 不满足条件时隐藏入口,避免用户进入一个无法工作的扫描流程。
还要在 Info.plist 里添加 camera usage description。演讲提醒,这段文案要解释为什么需要相机,让用户知道自己在授权什么。
创建扫描器
(04:40)创建扫描器时,最重要的参数是 recognizedDataTypes。它告诉系统要找什么。
官方示例先扫描二维码,也展示了如何改成文字、指定语言,或只找 URL。
import VisionKit
// Specify the types of data to recognize
let recognizedDataTypes:Set<DataScannerViewController.RecognizedDataType> = [
.barcode(symbologies: [.qr]),
// uncomment to filter on specific languages (e.g., Japanese)
// .text(languages: ["ja"])
// uncomment to filter on specific content types (e.g., URLs)
// .text(textContentType: .URL)
]
// Create the data scanner, present it, and start scanning!
let dataScanner = DataScannerViewController(recognizedDataTypes: recognizedDataTypes)
present(dataScanner, animated: true) {
try? dataScanner.startScanning()
}
关键点:
Set<DataScannerViewController.RecognizedDataType>可以同时包含机器码和文字。.barcode(symbologies: [.qr])把机器码识别限制为 QR code。.text(languages: ["ja"])可以给文字识别传语言提示,演讲中特别提到日语和韩语是 iOS 16 Live Text 新增支持的语言。.text(textContentType: .URL)可以让扫描器只寻找特定语义的文字,例如 URL。DataScannerViewController(recognizedDataTypes:)创建的是一个普通 view controller,可以全屏、sheet,或加入自定义层级。startScanning()在 presentation 完成后调用,开始从实时视频流里寻找数据。
(06:29)初始化参数还可以控制体验。qualityLevel 可选 balanced、fast、accurate。Apple 建议从 balanced 开始。大而清楚的文字可以用 fast,很小的二维码或序列号可以用 accurate。
let dataScanner = DataScannerViewController(
recognizedDataTypes: recognizedDataTypes,
qualityLevel: .balanced,
recognizesMultipleItems: true,
isHighFrameRateTrackingEnabled: true,
isPinchToZoomEnabled: true,
isGuidanceEnabled: true,
isHighlightingEnabled: true
)
关键点:
qualityLevel: .balanced是大多数场景的起点。recognizesMultipleItems: true允许一帧里识别多个项目,适合一次扫多个条形码。isHighFrameRateTrackingEnabled让自定义高亮更紧地跟随目标移动。isPinchToZoomEnabled控制用户是否可以双指缩放。isGuidanceEnabled显示顶部引导标签,帮助用户对准目标。isHighlightingEnabled开启系统高亮;如果要自己画高亮,可以关闭它。
处理用户点中的项目
(08:11)如果 App 需要知道用户点中了什么,就设置 delegate。
// Specify the types of data to recognize
let recognizedDataTypes:Set<DataScannerViewController.RecognizedDataType> = [
.barcode(symbologies: [.qr]),
.text(textContentType: .URL)
]
// Create the data scanner, present it, and start scanning!
let dataScanner = DataScannerViewController(recognizedDataTypes: recognizedDataTypes)
dataScanner.delegate = self
present(dataScanner, animated: true) {
try? dataScanner.startScanning()
}
关键点:
- 这里同时识别 QR code 和 URL 文本。
dataScanner.delegate = self让当前对象接收扫描器事件。- delegate 会收到点击、识别项新增、更新、移除等回调。
- present 完成后再启动扫描,避免扫描器尚未进入界面就开始工作。
(08:19)用户点击识别项时,delegate 收到 RecognizedItem。它是一个 enum,可能是文字,也可能是条码。
func dataScanner(_ dataScanner: DataScannerViewController, didTapOn item: RecognizedItem) {
switch item {
case .text(let text):
print("text: \(text.transcript)")
case .barcode(let barcode):
print("barcode: \(barcode.payloadStringValue ?? "unknown")")
default:
print("unexpected item")
}
}
关键点:
didTapOn在用户点击某个识别项时触发。.text(let text)分支读取文字识别结果。text.transcript是识别出的字符串。.barcode(let barcode)分支处理机器码。barcode.payloadStringValue只有 payload 能转成字符串时才有值,所以示例用?? "unknown"兜底。RecognizedItem还带有稳定的id和bounds,可用于跟踪和绘制高亮。
自定义高亮
(08:54)每个 RecognizedItem 都有 bounds。演讲特别说明,这个值由四个角点组成;因为透视变换下文字或条码可能呈梯形,所以不能把它当作普通矩形处理。
如果系统高亮不符合 App 风格,可以用三组 delegate 方法自己画。
(09:11)识别项刚出现时,创建高亮视图,并把它加到 overlayContainerView。
// Dictionary to store our custom highlights keyed by their associated item ID.
var itemHighlightViews: [RecognizedItem.ID: HighlightView] = [:]
// For each new item, create a new highlight view and add it to the view hierarchy.
func dataScanner(_ dataScanner: DataScannerViewController, didAdd addedItems: [RecognizedItem], allItems: [RecognizedItem]) {
for item in addedItems {
let newView = newHighlightView(forItem: item)
itemHighlightViews[item.id] = newView
dataScanner.overlayContainerView.addSubview(newView)
}
}
关键点:
itemHighlightViews用RecognizedItem.ID作为 key,把识别项和自定义高亮绑定起来。didAdd在新项目被识别时调用。newHighlightView(forItem:)根据识别项创建高亮视图。item.id在项目生命周期内保持稳定,直到它离开画面。overlayContainerView位于相机预览上方,适合承载自定义高亮。
(09:37)项目或相机移动时,更新高亮位置。
// Animate highlight views to their new bounds
func dataScanner(_ dataScanner: DataScannerViewController, didUpdate updatedItems: [RecognizedItem], allItems: [RecognizedItem]) {
for item in updatedItems {
if let view = itemHighlightViews[item.id] {
animate(view: view, toNewBounds: item.bounds)
}
}
}
关键点:
didUpdate在识别项移动、相机移动,或文字转写结果变化时调用。updatedItems只包含发生变化的项目。- 通过
item.id找回已有高亮视图。 item.bounds是新的四角坐标,用它驱动高亮动画。
(10:03)项目离开画面时,移除对应高亮。
// Remove highlights when their associated items are removed.
func dataScanner(_ dataScanner: DataScannerViewController, didRemove removedItems: [RecognizedItem], allItems: [RecognizedItem]) {
for item in removedItems {
if let view = itemHighlightViews[item.id] {
itemHighlightViews.removeValue(forKey: item.id)
view.removeFromSuperview()
}
}
}
关键点:
didRemove在项目不再可见时调用。removedItems给出刚刚离开场景的识别项。removeValue(forKey:)清掉字典里的引用。removeFromSuperview()把高亮从界面上移除。allItems参数包含当前仍然被识别的全部项目;文字识别场景下,这个数组按自然阅读顺序排列。
拍照和 AsyncStream
(10:54)扫描器也可以拍一张高质量静态图。示例把它保存到相册。
// Take a still photo and save to the camera roll
if let image = try? await dataScanner.capturePhoto() {
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
}
关键点:
capturePhoto()是异步方法。- 成功时返回
UIImage。 - 示例用
UIImageWriteToSavedPhotosAlbum保存到系统相册。 - 这个能力适合把扫描结果和现场照片一起存档,例如票据、货架或设备铭牌。
(11:10)如果不需要自定义高亮,也可以不用三组 delegate 方法。recognizedItems 提供一个持续更新的 AsyncStream。
// Send a notification when the recognized items change.
var currentItems: [RecognizedItem] = []
func updateViaAsyncStream() async {
guard let scanner = dataScannerViewController else { return }
let stream = scanner.recognizedItems
for await newItems: [RecognizedItem] in stream {
let diff = newItems.difference(from: currentItems) { a, b in
return a.id == b.id
}
if !diff.isEmpty {
currentItems = newItems
sendDidChangeNotification()
}
}
}
关键点:
currentItems保存上一轮识别结果。dataScannerViewController不存在时直接返回。scanner.recognizedItems是持续输出[RecognizedItem]的 async stream。for await随着场景变化接收新数组。difference(from:)用id比较项目是否相同。- 只有 diff 非空时才更新状态并发送通知。
核心启发
1. 仓库拣货扫码器
- 做什么:一次识别货架上的多个 QR code 或条形码,点中某个货品后进入拣货流程。
- 为什么值得做:
recognizesMultipleItems支持同一画面内多个识别项,didTapOn能拿到用户选中的项目。 - 怎么开始:把
recognizedDataTypes设为.barcode(...),开启多项目识别,用barcode.payloadStringValue匹配库存系统里的 SKU。
2. 票据 URL 和电话号码提取
- 做什么:用相机扫纸质票据或海报,只提取 URL 或电话号码。
- 为什么值得做:Data Scanner 支持文本 content type 过滤,减少无关文字进入业务流程。
- 怎么开始:使用
.text(textContentType: .URL)或对应的文本语义类型,用户点击识别项后读取text.transcript。
3. 现场资产盘点
- 做什么:扫描设备铭牌上的小号序列号,并拍下现场照片留档。
- 为什么值得做:
qualityLevel: .accurate适合小项目,capturePhoto()可以保存高质量静态图。 - 怎么开始:初始化时选择
.accurate,识别到序列号后调用await dataScanner.capturePhoto(),把图片和识别文本一起写入记录。
4. 品牌化扫描界面
- 做什么:关闭系统高亮,用自己的颜色、动画和形状标出识别项。
- 为什么值得做:
overlayContainerView和didAdd、didUpdate、didRemove提供了完整生命周期。 - 怎么开始:用
RecognizedItem.ID维护高亮字典,在didUpdate中根据item.bounds更新四角位置。
5. 实时文本看板
- 做什么:把相机当前看到的文字实时汇总到旁边的列表或调试面板。
- 为什么值得做:
recognizedItems是AsyncStream,适合把识别变化接入 Swift Concurrency 工作流。 - 怎么开始:在 task 中
for await读取scanner.recognizedItems,用difference(from:)过滤没有变化的帧。
关联 Session
- Add Live Text interaction to your app — Data Scanner 处理实时相机流;这场讲静态图片和暂停视频帧里的 Live Text 交互。
- Extract document data using Vision — Data Scanner 封装了常见实时扫描流程;这场展示如何直接使用 Vision 识别文档文字和条码。
- Create camera extensions with Core Media IO — 如果关注相机输入管线本身,这场讲 macOS 相机系统扩展。
- Discover PhotoKit change history — 扫描结果常会落到照片或图像工作流里;这场讲如何跟踪照片库变化。
评论
GitHub Issues · utterances