将视觉智能整合到 App 中的最佳做法
Best practices for integrating visual intelligence in your app
2026年6月10日
一句话判断
这场 Session 最值得关注的一件事是:Visual Intelligence(视觉智能)终于开放了基于本地图像特征的自定义搜索接入,你的 App 现在可以直接“看懂”用户框选的屏幕截图或相机画面,并把业务数据塞进系统的原生结果页了。
这场 Session 讲了什么
以前 Visual Intelligence 基本是系统级特权,识别个植物、查个商品价格,第三方开发者只能在旁边看着。现在 Apple 把 SemanticContentDescriptor(语义内容描述符)和 IntentValueQuery(意图值查询)打通了。系统会把用户框选的图像转成 pixelBuffer(像素缓冲区)直接喂给你的 App。
你拿到像素数据后,用 Vision 框架在本地跑图像相似度匹配,然后通过 @UnionValue 把多种业务实体(比如音乐 App 里的专辑和演唱会)混排返回给系统。对电商、媒体、票务这类内容驱动型 App 来说,这相当于在系统相机和截图界面开了一个免唤醒的超级流量入口,不用用户切 App 就能直接完成“看图-发现-购买/播放”的闭环。
值得深挖的点
本地图像检索的工程妥协与性能陷阱
Apple 选择把 pixelBuffer 传给 App 并在端上用 GenerateImageFeaturePrintRequest(生成图像特征打印请求)做匹配,而不是给个图片 URL 让你传后端,这个设计选择非常明确:死保隐私和响应速度。系统不希望你的服务器拿到用户的屏幕截图或相册原图,同时网络请求的延迟在 Visual Intelligence 这种“即框即搜”的场景下是不可接受的。
这就要求开发者必须在端上预计算好业务数据的 FeaturePrintObservation(特征打印观察结果)。这里有个巨大的坑:Session 里轻描淡写地提到了 Mac 和 iPad 的适配。在 Mac 上,用户框选的截图 pixelBuffer 尺寸可能是 iPhone 的数倍(比如 5K 显示器上的截图)。如果你直接把这个巨型 buffer 丢给 Vision 框架去算特征,或者做全图匹配,App 会瞬间 OOM(内存溢出)或者卡死主线程。必须在端上写降采样逻辑,把输入图像缩放到合理的 thumbnail(缩略图)尺寸再去算 distance(距离)。
用 @UnionValue 打破单一实体的搜索天花板
以前写 App Intents,返回的数组只能是同一种 AppEntity(应用实体)。但在真实的视觉搜索场景里,用户拍一张海报,可能既想搜原声带(专辑),又想搜巡演(演唱会)。这次引入的 @UnionValue(联合值)完美解决了这个问题。
你可以定义一个包含多种 Entity 的枚举,让 IntentValueQuery 返回这个联合类型。配合 semanticContentSearch(语义内容搜索)schema,你甚至可以在系统结果页的底部挂一个“在 App 内继续搜索”的兜底入口。当系统的图像匹配阈值(比如 distance > 1.0)没命中合适结果时,用户点击这个入口,系统会把当前的 SemanticContentDescriptor 原封不动地传给你的 App 内搜索页,直接预填上下文。这种把系统级流量平滑过渡到 App 内深度浏览的设计,比生硬地弹一个 OpenURL 优雅太多了。
代码片段
1. 定义多结果类型的联合查询
场景:让视觉搜索同时返回专辑和演唱会两种不同维度的业务数据。
import AppIntents
import VisualIntelligence
// 定义联合值,打破单一 Entity 返回限制
@UnionValue
enum VisualSearchResult {
case album(AlbumEntity)
case concert(ConcertEntity)
}
struct SearchHandler: IntentValueQuery {
@Dependency var catalog: AlbumCatalog
@Dependency var concertFinder: ConcertFinder
// 接收系统传入的语义内容描述符
func values(for input: SemanticContentDescriptor) async throws -> [VisualSearchResult] {
guard let pixelBuffer = input.pixelBuffer else { return [] }
// 在本地目录中匹配视觉相似的专辑
let albums = try await catalog.search(matching: pixelBuffer)
let artists = albums.map { $0.artistName }
// 根据匹配到的艺术家,顺藤摸瓜找相关演唱会
let concerts = await concertFinder.findConcerts(for: artists)
// 混排返回两种实体
return albums.map { .album($0) } + concerts.map { .concert($0) }
}
}
坑:values(for:) 方法对执行时间有严格限制,如果 concertFinder 涉及网络请求,极大概率会超时导致系统直接丢弃你的结果,尽量用本地缓存。
2. 预计算与匹配图像特征
场景:使用 Vision 框架在端上完成图像相似度计算,避免网络延迟。
import Vision
func search(matching pixelBuffer: CVReadOnlyPixelBuffer, maxDistance: Double = 1.0) async throws -> [AlbumEntity] {
// 将 pixelBuffer 转为 CGImage
var cgImage: CGImage?
_ = pixelBuffer.withUnsafeBuffer {
VTCreateCGImageFromCVPixelBuffer($0, options: nil, imageOut: &cgImage)
}
guard let inputImage = cgImage else { return [] }
// 生成输入图像的特征向量
let queryPrint = try await generateFeaturePrint(for: inputImage)
// 与预计算好的目录特征进行比对
return try catalog.entries.compactMap { entry -> (album: AlbumEntity, distance: Double)? in
// 计算两个特征向量的距离,值越小越相似
let distance = try queryPrint.computeDistance(to: entry.featurePrint)
guard distance < maxDistance else { return nil }
return (entry.album, distance)
}
// 按相似度排序并截取前 10 个
.sorted { $0.distance < $1.distance }
.prefix(10)
.map { $0.album }
}
坑:Mac 端的 pixelBuffer 尺寸巨大,直接转 CGImage 并丢给 generateFeaturePrint 会引发内存峰值。务必在转换前或转换后对图像进行降采样(Downsampling)。
最佳实践
已有项目的迁移策略:不要试图把后端的图像识别 API 直接包装成 App Intent。Visual Intelligence 要求的是毫秒级响应,网络 IO 根本来不及。把核心商品或内容的特征向量(Feature Print)随业务数据一起下发,在 App 启动或后台刷新时预计算并持久化到本地 SQLite 或 Core Data 中。
新项目的采用建议:DisplayRepresentation(展示表示)里的缩略图千万不要用原图 URL。系统结果页是双列卡片布局,加载 2MB 的高清封面只会让滚动掉帧。专门准备一套 200x200 级别的 thumbnail(缩略图)数据,或者在端上裁剪好再传给 DisplayRepresentation。
实战中容易踩的坑:OpenIntent(打开意图)的执行环境很特殊,它是在 App 被唤起并进入前台的过程中运行的。不要在这个 Intent 的 perform 方法里做重度初始化或弹复杂的 Modal,直接修改全局的 AppState(应用状态)或路由栈,让 UI 层自己去响应状态变化。
还有什么值得关注
- System Store 自动监听:通过 EventKit(事件工具包)或 HealthKit(健康工具包)写入的数据,App 只要监听 store 变化通知就能自动刷新 UI,不用自己苦哈哈地写 OCR 去解析日历截图了。
- 语义搜索兜底路由:
semanticContentSearchschema 允许你在系统结果页底部挂一个“在 App 内搜索”的入口,把系统没匹配上的长尾流量无缝引流回 App 原生搜索页。 - 跨平台像素差异:iPad 和 Mac 的输入源不仅是相机,更多是截图。Mac 截图的
pixelBuffer尺寸是 iPhone 的数倍,不做降采样直接跑 Vision 请求必崩。