Highlight
Apple 在 iOS、iPadOS 和 macOS 上开放了 Visual Intelligence 的 Image Search 接入能力。开发者通过 App Intents 定义业务实体,用 Vision 框架在端上完成图像相似度匹配,即可让用户在相机或截图中框选内容时直接看到 App 的搜索结果,并一键跳转到对应页面。
核心内容
从”只能看”到”能参与”
以前 Visual Intelligence 是系统独占功能。用户拍植物、查商品,结果页里只有系统提供的信息,第三方 App 插不上手。WWDC26 把这个能力开放了——你的 App 可以成为 Visual Intelligence 的搜索结果来源之一。
想象一个场景:朋友发来一张专辑封面截图,你想立刻在收藏里找到这张专辑开始播放。以前的做法是保存图片、打开 App、手动搜索。现在用户只需框选截图中的封面,你的 App 就能直接返回匹配结果,点击即播。
这个流程能跑通,靠的是三个组件的配合:App Entity 定义要返回什么内容,IntentValueQuery 接收图像并返回结果,OpenIntent 把用户带到正确的页面。
跨平台,同一套代码
(10:08)
Visual Intelligence 今年扩展到了 iPadOS 和 macOS。好消息是,IntentValueQuery、Entity 和 OpenIntent 的代码在三端通用,不需要为每个平台单独写一套。
但要注意使用场景的差异。iOS 上用户主要通过相机拍摄物理世界——唱片封面、演唱会海报。macOS 和 iPad 上则更多是截图,框选屏幕上的数字内容。Mac 上的截图像素尺寸可能远大于 iPhone,直接处理原始像素会吃光内存,需要先做缩放。
不只返回一种结果
(11:47)
用户框选一张海报,可能既想听专辑,又想了解相关演唱会。@UnionValue 让单个查询返回多种实体类型成为可能。你可以把专辑和演唱会混排在一个结果列表里,每种类型对应自己的 OpenIntent,点击后进入不同的页面。
系统商店的数据回流
(14:27)
Visual Intelligence 不只是”你的 App 给系统提供结果”这一个方向。系统也能把提取到的数据写入共享存储,你的 App 读取即可。
比如用户用 Visual Intelligence 识别了社交媒体上的演唱会信息并添加到日历,你的 App 通过 EventKit 监听日历变化,就能自动在”即将开始的演唱会”列表里显示这条记录。Contacts、HealthKit 也是同样的模式。
详细内容
定义 App Entity
(03:21)
第一步是用 App Intents 框架定义要返回的实体。以下是一个音乐 App 中专辑实体的完整定义:
import AppIntents
struct AlbumEntity: AppEntity {
var id: String
@Property var name: String
@Property var artistName: String
var coverArtData: Data
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(
title: "\(name)",
subtitle: "\(artistName)",
image: .init(data: coverArtData)
)
}
static let defaultQuery = AlbumEntityQuery()
static var typeDisplayRepresentation: TypeDisplayRepresentation { "Album" }
}
struct AlbumEntityQuery: EntityQuery {
@Dependency var catalog: AlbumCatalog
func entities(for identifiers: [String]) async throws -> [AlbumEntity] {
catalog.albums(for: identifiers)
}
}
关键点:
DisplayRepresentation控制在结果页中如何展示。系统给的空间大约三行文字加一张缩略图,把最重要的识别信息放在标题和副标题里。- 如果返回多个结果,缩略图会出现在双列布局中,建议提供 200x200 左右的小图,不要用原图 URL。
- 如果只返回一个结果,图片会占满结果页整宽,这时可以用分辨率稍高的图。
实现 IntentValueQuery 接收图像输入
(05:39)
Visual Intelligence 通过 IntentValueQuery 协议把捕获的图像传给 App。输入类型是 SemanticContentDescriptor,里面包含 pixelBuffer:
import AppIntents
import VisualIntelligence
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)
return albums.map { VisualSearchResult.album($0) }
}
}
关键点:
values(for:)接收SemanticContentDescriptor,从中提取pixelBuffer。- 查询有严格的执行时间限制,不要做网络请求或重度计算。
- 如果没有匹配结果,返回空数组即可,系统会处理空状态的展示。
用 Vision 框架做本地图像匹配
(06:24)
端上搜索的核心是 GenerateImageFeaturePrintRequest,它把图像转成紧凑的特征向量(feature print),通过计算向量距离判断相似度:
import Vision
@Observable
class AlbumCatalog {
static let shared = AlbumCatalog()
struct CatalogEntry: Sendable {
let album: AlbumEntity
let featurePrint: FeaturePrintObservation
}
private(set) var entries: [CatalogEntry] = []
private func generateFeaturePrint(for image: CGImage) async throws -> FeaturePrintObservation {
let request = GenerateImageFeaturePrintRequest()
let result = try await request.perform(on: image)
return result
}
}
搜索时,先为输入图像生成 feature print,再与预计算好的目录比对:
func search(matching pixelBuffer: CVReadOnlyPixelBuffer, limit: Int = 10, maxDistance: Double = 1.0) async throws -> [AlbumEntity] {
var cgImage: CGImage?
_ = pixelBuffer.withUnsafeBuffer {
VTCreateCGImageFromCVPixelBuffer($0, options: nil, imageOut: &cgImage)
}
guard let cgImage else { return [] }
let queryPrint = try await generateFeaturePrint(for: cgImage)
return try entries.compactMap { entry -> (album: AlbumEntity, distance: Double)? in
let distance = try queryPrint.distance(to: entry.featurePrint)
guard distance <= maxDistance else { return nil }
return (entry.album, distance)
}
.sorted { $0.distance < $1.distance }
.prefix(limit)
.map { $0.album }
}
关键点:
- 用
VTCreateCGImageFromCVPixelBuffer把CVPixelBuffer转成CGImage。 FeaturePrintObservation.distance(to:)计算两个特征向量的距离,值越小越相似。maxDistance过滤掉不相关的结果,避免返回噪声。- 目录中的 feature print 必须预计算,查询时只做距离比较,保证响应速度。
- Mac 上的截图尺寸可能很大,建议在转
CGImage之前先缩放,防止内存峰值。
用 OpenIntent 跳转到正确页面
(08:27)
用户点击搜索结果后,系统调用对应的 OpenIntent:
import AppIntents
struct OpenAlbumIntent: OpenIntent {
static let title: LocalizedStringResource = "Open Album"
@Parameter(title: "Album")
var target: AlbumEntity
@Dependency var appState: AppState
func perform() async throws -> some IntentResult {
await appState.openAlbum(id: target.id)
return .result()
}
}
关键点:
perform()在 App 进入前台的过程中执行,只做轻量导航操作。- 重度数据加载留到视图出现后再做,避免阻塞启动流程。
- 如果你之前为 Siri 集成已经写过 OpenIntent,可以直接复用。
返回多种结果类型
(12:05)
用 @UnionValue 让单个查询返回多种实体:
@UnionValue
enum VisualSearchResult {
case album(AlbumEntity)
case concert(ConcertEntity)
}
struct OpenConcertIntent: OpenIntent {
static let title: LocalizedStringResource = "Open Concert"
@Parameter(title: "Concert")
var target: ConcertEntity
@Dependency var appState: AppState
func perform() async throws -> some IntentResult {
await appState.openConcert(id: target.id)
return .result()
}
}
更新查询逻辑,把两种结果混排:
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.findNearby(byArtists: artists)
return albums.map { VisualSearchResult.album($0) }
+ concerts.map { VisualSearchResult.concert($0) }
}
}
关键点:
@UnionValue枚举的每种 case 包裹一个 AppEntity。- 每种实体类型需要独立的 OpenIntent。
- 结果混排顺序由你控制,建议把最相关的结果放前面。
提供 App 内搜索的兜底入口
(13:13)
如果系统结果页没有用户想要的内容,可以提供一个”在 App 内继续搜索”的入口:
@AppIntent(schema: .visualIntelligence.semanticContentSearch)
struct SemanticContentSearchIntent: AppIntent {
static let title: LocalizedStringResource = "Search in app"
static let openAppWhenRun: Bool = true
var semanticContent: SemanticContentDescriptor
@Dependency var catalog: AlbumCatalog
@Dependency var concertFinder: ConcertFinder
@Dependency var appState: AppState
func perform() async throws -> some IntentResult {
guard let pixelBuffer = semanticContent.pixelBuffer else { return .result() }
let albums = try await catalog.search(matching: pixelBuffer)
let artists = albums.map { $0.artistName }
let concerts = await concertFinder.findNearby(byArtists: artists)
await appState.openSearch(albums: albums, concerts: concerts)
return .result()
}
}
关键点:
- 使用
.visualIntelligence.semanticContentSearchschema,系统会自动传入semanticContent。 openAppWhenRun: true确保 App 被唤起。- 在 App 内预填充搜索结果,用户不用从零开始。
通过 EventKit 接收系统日历数据
(15:24)
用户通过 Visual Intelligence 添加到日历的事件,App 可以通过 EventKit 读取:
import EventKit
@Observable
class UpcomingConcertManager {
private let eventStore = EKEventStore()
var upcomingConcerts: [EKEvent] = []
var authorizationStatus: EKAuthorizationStatus = .notDetermined
func requestAccessAndFetch() async throws {
let granted = try await eventStore.requestFullAccessToEvents()
guard granted else {
authorizationStatus = .denied
return
}
authorizationStatus = .fullAccess
await fetchUpcomingConcerts()
for await _ in NotificationCenter.default.notifications(named: .EKEventStoreChanged) {
await fetchUpcomingConcerts()
}
}
func fetchUpcomingConcerts() async {
let predicate = eventStore.predicateForEvents(
withStart: .now,
end: .now.addingTimeInterval(90 * 24 * 60 * 60),
calendars: nil
)
let events = eventStore.events(matching: predicate)
upcomingConcerts = events.filter { event in
AlbumCatalog.shared.entries.contains { entry in
event.title?.localizedCaseInsensitiveContains(entry.album.artistName) == true
}
}
}
}
关键点:
requestFullAccessToEvents()请求完整日历访问权限。- 监听
.EKEventStoreChanged通知,Visual Intelligence 添加的新事件会自动触发刷新。 - 用
predicateForEvents查询未来 90 天内的事件,再按业务逻辑过滤。
核心启发
1. 做一个”拍照找商品”的电商入口
- 做什么:用户拍下杂志上的商品照片,App 直接返回匹配的商品卡片。
- 为什么值得做:Visual Intelligence 把相机变成了免唤醒的搜索入口,用户不用打字、不用切换 App。
- 怎么开始:用
GenerateImageFeaturePrintRequest为商品图预计算 feature print,存到本地数据库。定义ProductEntity,实现IntentValueQuery做相似度匹配。
2. 做一个”截图识别票务信息”的演出助手
- 做什么:用户截图社交媒体上的演唱会海报,Visual Intelligence 识别后添加到日历,App 自动在”即将观看”列表中显示,并推荐预热歌单。
- 为什么值得做:系统商店集成让数据自动回流,App 不需要自己写 OCR 解析截图。
- 怎么开始:监听 EventKit 的
.EKEventStoreChanged,过滤出与 App 业务相关的事件。
3. 做一个”框选屏幕文字”的跨应用笔记工具
- 做什么:用户在任意 App 中框选一段文字或图片,你的笔记 App 接收
SemanticContentDescriptor,提取内容并创建笔记条目。 - 为什么值得做:macOS 上的截图是主要输入源,桌面工作流中跨应用收集信息的痛点很真实。
- 怎么开始:结合 Vision 的文本识别(
RecognizeTextRequest)从pixelBuffer提取文字内容。
4. 做一个”拍照识别药品”的健康记录工具
- 做什么:用户拍摄血压计、血糖仪的屏幕,Visual Intelligence 把读数写入 HealthKit,App 自动同步到健康档案。
- 为什么值得做:WWDC26 提到医疗设备日志是 Visual Intelligence 的新能力,HealthKit 读取即可,不需要自己解析数字。
- 怎么开始:用
HKHealthStore查询相关类型的健康数据,监听数据变化。
5. 做一个”框选海报”的多媒体发现引擎
- 做什么:用户框选电影海报,App 返回电影信息、原声带、附近影院排片,三种结果混排在一个列表里。
- 为什么值得做:
@UnionValue让单一查询返回多种实体,一个动作触发多条业务线。 - 怎么开始:定义包含电影、音乐、影院三种 case 的
@UnionValue枚举,每种配独立的 OpenIntent。
关联 Session
- Bring your app to Siri — App Intents 基础入门,如果你还没接触过 App Entity 和 Intent,先看这场。
- What’s new in App Intents — 了解 App Intents 的新特性,包括 UnionValue 的更多用法。
- What’s new in Vision — 深入了解 Vision 框架的图像处理能力,包括 feature print、文本识别、条码扫描等。
- Build agentic apps with App Intents — 探索 App Intents 在智能化应用中的高级用法。
- What’s new in SwiftUI — Visual Intelligence 的结果页需要配合 SwiftUI 的导航和状态管理来落地页面跳转。
评论
GitHub Issues · utterances