-
将视觉智能整合到 App 中的最佳做法
深入了解视觉智能如何改变你 App 中的内容发现体验。探索如何定义实体、处理图像,并高效管理多种结果类型。了解优化速度和相关性的最佳做法,并探索意图如何实现一键打开或播放内容等直接操作。
章节
- 0:07 - Introduction
- 2:02 - Defining your content
- 5:03 - Implementing a query
- 8:18 - Opening results
- 10:03 - Mac and iPad adoption
- 12:27 - Returning multiple result types
- 12:56 - Continuing search in your app
- 14:27 - System store integrations
- 17:16 - Next steps
资源
-
搜索此视频…
嗨,我是 David,
系统体验团队的机器学习工程师。
让我们一起用 Visual Intelligence 来构建一些东西。
在本节课中, 我将带你一步步了解 如何将你的 App 与 Visual Intelligence 集成, 并在过程中分享 一些最佳实践。 自从 Visual Intelligence 推出以来, 人们一直在使用它 快速了解更多 关于周围事物的信息, 无论是现实环境中的事物, 还是 iPhone 屏幕上的内容。
今年,我们正在添加新功能, 例如添加到通讯录、 保存多个日历事件 以及医疗设备日志记录,
同时将 Visual Intelligence 引入 iPad 和 macOS。
那么,如何让你的 App 融入这一体验呢? 我将通过构建一个 App 来展示。
我热爱聆听和发现新音乐。 所以我想创建一个 App, 帮助我发现专辑 并找到即将到来的演唱会。
这就是我们今天要构建的内容。 这是我的音乐 App。 我可以浏览专辑、 查看即将到来的演唱会, 并轻点任意内容 即可开始播放。 如果我拍下或截取 某张专辑封面的照片, 并高亮选中进行搜索, 我的 App 会在 Visual Intelligence 中 直接显示匹配的专辑和演唱会。 我甚至可以拍下 一篇关于即将到来的演唱会的帖子,
使用 Visual Intelligence 将活动添加到我的日历,
演唱会就会 自动出现在我的 App 中。 在本节课结束时, 你将学会如何构建这一切。 有几个步骤 可以充分利用你的 App 与 Visual Intelligence 的集成, 我将在今天一一介绍。 首先,我们将定义 希望从 App 返回的内容, 使用 App entities。
接下来,我们将实现一个查询, 让 Visual Intelligence 能够找到 并返回我们 App 的内容。
然后,我们将把集成扩展到 iOS 之外——引入 Mac 和 iPad, 并在过程中涵盖 每个平台的一些注意事项。
最后,我们将探索 系统存储集成, 即 Visual Intelligence 提取的信息 可以被你的 App 自动读取, 来自你可能已经采用的 常见数据存储。 让我们从图像搜索的 基础知识开始。 集成图像搜索同时利用了 App Intents 和 Visual Intelligence 框架。 如果你是 App Intents 的新手, 我建议查看 WWDC25 的这些视频。
集成图像搜索的第一步 是定义我们想要返回的内容。
我们将使用 App Intents 框架中的 App entities 来实现这一点。 App entities 是你 App 中的名词。 在我们的 App 中,我希望图像搜索 首先返回视觉上相似的专辑, 所以我将定义一个专辑实体。
让我们看看这在代码中是什么样子。
我们将从定义一个 AlbumEntity 开始, Visual Intelligence 可以在 其搜索结果中显示该实体。
首先,我将添加一个默认的 EntityQuery 和一个 typeDisplayRepresentation, 这对任何 App entity 都是标准的。
然后我将定义实体的内容。
每个 AlbumEntity 都有一个标识符、 名称、artistName 和专辑封面的 缩略图数据。
我还将添加一个 displayRepresentation, 它告诉 Visual Intelligence 如何呈现每个结果。
让我们谈谈我们刚刚定义的 display representation。
这是人们在图像搜索结果中 首先看到的内容。 空间并不多—— 标题和副标题大约有三行文字, 以及一张缩略图。
最好将最重要的 识别信息放在这里。 在我的情况下,就是专辑名称和艺术家。
如果你用图像 URL 初始化一个 display representation,
我建议在适当时候 提供缩略图大小的图像, 而不是指向 你的完整分辨率资源。
例如,如果你总是期望 返回多个结果, 使用较小的图像可以帮助 你的结果加载更快, 并且在双列布局中 仍然看起来不错。
但是,如果你只返回一个结果, 请记住,这张图像将占据 结果表单的全部宽度。
现在我已经定义了我的实体, Visual Intelligence 如何 实际查询我的 App 以获取结果? 这就是 Intent value query 的用武之地。 Intent value query 是一种轻量级的 查询协议, 用于向系统提供实体值。
你可能已经有一个了, 如果你已采用 App Intents 使你的 App 与 Siri 配合使用。
对于 Visual Intelligence, 关键区别在于输入—— 系统传递一个 SemanticContentDescriptor, 其中包含有关捕获图像的信息。 让我们回到代码来构建这个。
我将采用 IntentValueQuery 协议 并以 SemanticContentDescriptor 作为输入 实现其 values for requirement。
在函数体中,我将从输入中 获取 pixelBuffer, 并将其传递给我的 catalog.search 方法, 该方法返回匹配的专辑。
但这个搜索实际上是如何工作的呢? 让我们接下来看看。
对于这个 App,我将在设备上 使用已保存专辑的本地目录进行搜索。
我将使用 Vision 框架, 它为计算机视觉任务 提供预训练的机器学习模型。
我们目录中的每个条目 都将有一个 featurePrint, 这是图像的紧凑数值表示, 我们可以用它来 比较图像相似性。
我将定义一个函数 来计算特征印记, 使用 GenerateImageFeaturePrintRequest。
我将确保为目录中的专辑 预先计算这些, 这样我们就不需要 在查询时进行此计算。
对于我们的查询,我将首先 使用 VideoToolbox 将 pixelBuffer 转换为 CGImage。 然后,我将为这张图像 生成一个新的特征印记。
我将把它与目录中 预先计算的特征印记进行比较, 应用最大距离阈值 来过滤掉不相似的结果。
最后,我按相似度排序 并返回最相关的结果。
有几点需要注意。 我为我的专辑目录 预先计算特征印记, 以保持查询速度。
我按相似度对结果排序, 使最佳匹配排在第一位。 无论你是在设备上搜索 还是访问服务器, 同样的原则适用—— 快速返回并排序结果。
我还建议限制 返回的结果数量, 以确保其相关性。
如果没有找到好的匹配项, 你可以返回一个空数组。 系统将处理 显示空响应的情况。
我也鼓励你查看 Vision 框架 API, 以了解更多可以在 你的 App 中使用的图像处理技术。 我们只是初步了解了特征印记, 但你可以做更多的事情, 比如提取文本、 扫描条形码、检测人脸, 以及分类图像,仅举几例。 这些都是非常有用的技术, 可以扩展你的 App 视觉搜索的能力。
现在,当用户点击结果时, 我们如何引导他们 进入 App 的正确页面? 为此,我们需要一个 OpenIntent。 当有人在图像搜索结果中 点击某张专辑时, 系统会使用所选实体 调用此 intent。
我的 perform 方法 会导航到专辑详情页面。
你的 OpenIntent 应该直接 将用户带到他们选择的内容。
如果你已经为实体 设置了一个 OpenIntent, 是为了采用 App Intents 来支持其他功能, 你也可以在这里重用它。 你不需要为 Visual Intelligence 单独创建一个。
建议保持这个轻量化。 当 App 进入前台时 会运行此方法, 所以请完成你的导航, 并将任何繁重的加载 留到视图出现之后。
这就是基本图像搜索集成 所需的全部内容。 让我们来看看 我们目前构建的成果。
我朋友给我发了这个推荐, 让我们使用 Visual Intelligence 在我们的 App 中开始收听它。 我将截取屏幕截图, 高亮选中进行搜索, 并从可用的提供方中 选择我们的 App。
我们的查询成功了, 我们找到了专辑 并将其作为最相关的结果返回。
值得一提的是,你的 App 会与其他采用此功能的 App 一起显示。
系统根据以下因素决定排序: 设备上哪些 图像搜索提供方可用。 点击这个结果后, 它会直接带我进入 App 中的专辑页面。
这就是我们的实体、查询 和 OpenIntent 协同工作的效果。 现在,让我们将其带到更多平台。 今年, Visual Intelligence 也将 在 iPadOS 和 macOS 上提供。 这些新平台上 也提供相同的 API, 只需对你的 App 做极少改动。
你的 IntentValueQuery、你的实体 和你的 OpenIntent 都能在 iOS、iPadOS 和 macOS 上运行。 这就是我们刚刚编写的代码。
话虽如此, 有一些平台差异 值得牢记。
在 iOS 上,人们通常 通过相机使用 Visual Intelligence—— 拍摄黑胶唱片或 演唱会海报等实物。
在 macOS 和 iPad 上, 主要入口是截图——拍摄数字媒体。
确保你的搜索能很好地 处理这两种类型的内容。
还要记住,在 Mac 上, 输入的像素缓冲区可能比 在 iPhone 上遇到的大得多。
考虑是否需要 针对你的用例调整大小。
让我们为 macOS 构建我们的 App 并查看效果。
我将截取同一张图像的截图, 在不更改我们的 查询或实体代码的情况下, 我们 App 的图像搜索在 macOS 上运行了。
结果看起来很棒。
现在我们已经了解了基础知识, 我想为我们的 App 添加更多功能。
如果我们不仅可以搜索 视觉上相似的专辑, 还可以搜索这些专辑艺术家的 即将到来的演唱会呢? 为此,我们可以使用 UnionValue。
由于我们的 App 只能有 一个 IntentValueQuery 接受 SemanticContentDescriptor, 我将定义一个 @UnionValue 枚举, 为每种实体类型——专辑和演唱会——设置一个 case。
由于我现在有两种实体类型, 我需要为每种类型设置一个 OpenIntent。
然后我将更新我的查询 以返回此联合类型。
我将首先搜索 最匹配的专辑, 然后使用这些专辑的艺术家 来查找附近的演唱会, 并将它们合并 到一个结果列表中。
考虑一下你的 App 返回多种类型结果是否合理。
值得考虑 你的 App 可以返回的 不同类型内容, 而不仅仅是匹配像素。 我通过图像相似性找到了专辑, 然后使用这些艺术家名称 来显示附近的演唱会, 这是一种完全不同类型的结果。
请随意发挥创意, 根据上下文 决定返回哪种类型的内容。
最后,如果人们没有立即找到 他们想要的结果, 我想提供一种简单的方式 让他们继续在 App 内搜索。
我们可以使用 semanticContentSearch schema 来实现。 我将创建一个符合 semanticContentSearch schema 的 intent。
系统会自动提供 semanticContent 属性。 这就是我们之前看到的带有 像素缓冲区的 SemanticContentDescriptor。
在 perform 中,我将 导航到应用内搜索视图, 并预填充一些搜索结果。
现在,当有人点击"更多结果"时, 他们将进入我 App 的 完整搜索体验。 使用 semantic content search 是一个很好的做法, 可以为用户提供一种 延续到你完整搜索体验的方式。
你可以根据输入上下文 预填充你的搜索视图, 而不是从头开始。
你的 App 可以显示比 Visual Intelligence 结果视图更多的内容—— 过滤器、类别、 你内容的完整深度。 充分利用这一点。
让我们看看我们 构建的一切实际运行的效果。
我将再次截取 这张专辑的截图, 我的 App 会在 Visual Intelligence 中 直接返回匹配的专辑和演唱会。
如果我想浏览更多, 点击"更多结果"按钮 会带我进入 App 的完整搜索。
我们已经讨论了你的 App 向 Visual Intelligence 提供结果。 但这个故事还有另一面。
你的 App 也可以 从 Visual Intelligence 接收数据, 通过系统存储集成。 向 Visual Intelligence 提供结果 是通过图像搜索集成完成的, 这就是我们目前构建的全部内容。
其他 Visual Intelligence 操作 将数据写入系统存储, 这为开发者提供了 与共享系统数据的桥梁。
事件可以通过 EventKit 读取, 联系人信息可以通过 Contacts 读取, 医疗设备读数 可以通过 HealthKit 读取。 如果你的 App 已经从 这些框架的数据存储中读取, Visual Intelligence 会自动成为 新的输入来源。
对于我们的 App,我想了解 人们感兴趣的即将到来的演唱会, 这样我们可以建议 他们提前收听相关歌曲。 所以让我们添加一个 EventKit 集成 来访问这些事件。
这是我的 UpcomingConcertManager, 它使用 EKEventStore。
我将请求日历的读取权限, 然后查询即将到来的事件。
对于我们的 App,我只需 筛选近期的事件, 这些事件与我目录中的艺术家匹配。
我还将添加一个通知观察者, 这样新事件,包括由 Visual Intelligence 创建的事件, 会自动出现。
现在让我们看看最后一个部分。
当我拍下这篇关于 即将到来的演唱会的社交媒体帖子时, Visual Intelligence 会检测到该事件, 让我可以将其添加到日历。
当我打开我的 App 时, 它已经出现在"即将到来的演唱会"中, 并有一个开始收听的建议。
同样的模式适用于 其他系统存储。
通过 Visual Intelligence 添加的联系人, 例如来自名片的联系人, 可以通过 CNContactStore 访问。
由 Visual Intelligence 捕获的 医疗设备读数, 来自血压监测仪的显示屏, 血糖仪或体重秤, 可以使用 HKHealthStore 查询。 如果你的健康或健身 App 从 HealthKit 读取数据, Visual Intelligence 就成为 人们记录数据的另一种方式, 无需手动输入。 我们今天涵盖了很多内容。 回顾一下,Visual Intelligence 为 你的 App 提供两个强大的集成点。
你可以向 Visual Intelligence 提供结果, 通过图像搜索, 以及你可以从 Visual Intelligence 接收数据, 通过系统存储集成。 随着 Visual Intelligence 现已在 iOS、iPadOS 和 macOS 上可用, 你的集成可以跨设备 触达用户。
如果你想了解更多, 请查看开发者网站上 提供的文档。
你还可以查看这些相关视频, 以进一步了解 App Intents 和 Vision 框架的功能。
感谢收看。 迫不及待想看到你们 用 Visual Intelligence 打造的出色作品。
-
-
3:21 - Define the content you want to return as an App Entity
// Define the content you want to return as an App Entity 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) } } -
5:39 - Adopt IntentValueQuery to return results
// Adopt IntentValueQuery to return visual search results 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) } } } -
6:24 - Build a catalog of albums with precomputed feature prints
// Build a catalog of albums with precomputed feature prints 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 } } -
6:45 - Search the catalog for albums matching the captured image
// Search the catalog for albums matching the captured image 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 } } -
8:27 - Create an open intent to land users on the right screen
// Create an open intent to land users on the right screen 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() } } -
12:05 - Use UnionValue to return multiple visual search result types
// Use UnionValue to return multiple visual search result types @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() } } -
12:18 - Expand the IntentValueQuery to return the UnionValue
// Expand the IntentValueQuery to return the UnionValue 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) } } } -
13:13 - Provide a link to in-app search
// Provide a link to in-app search @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() } } -
15:24 - Request calendar access and fetch upcoming concerts
// Request calendar access and fetch upcoming concerts 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() // ... } } -
15:42 - Filter for upcoming events that match known artists in our catalog
// Filter for upcoming events that match known artists in our catalog class UpcomingConcertManager { 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 } } } } -
15:44 - Observe newly created events
// Observe newly created events @Observable class UpcomingConcertManager { // ... func requestAccessAndFetch() async throws { // ... for await _ in NotificationCenter.default .notifications( named: .EKEventStoreChanged ) { await fetchUpcomingConcerts() } } }
-
-
- 0:07 - Introduction
Visual Intelligence integration and what's new in iOS 26, iPadOS, and macOS, using a sample music-discovery app built throughout the session. Outlines the agenda: defining content, implementing a query, cross-platform adoption, and system store integrations.
- 2:02 - Defining your content
Model your app's content as an AppEntity so Visual Intelligence can display it in search results. Covers the entity's DisplayRepresentation (title, subtitle, thumbnail) and best practices around concise identifying text and thumbnail-sized images.
- 5:03 - Implementing a query
IntentValueQuery returns results from a SemanticContentDescriptor's pixel buffer — using the Vision framework's GenerateImageFeaturePrintRequest for on-device image similarity, with pre-computed feature prints and distance thresholds to keep results fast.
- 8:18 - Opening results
Implement an OpenIntent to take people straight to the selected content. Keep it lightweight since it runs as the app foregrounds, and reuse an existing OpenIntent rather than creating one specific to Visual Intelligence.
- 10:03 - Mac and iPad adoption
The same entities, query, and OpenIntent carry over to iPadOS and macOS with minimal changes. Account for platform differences such as camera versus screenshot input and the much larger pixel buffers on Mac that may need resizing.
- 12:27 - Returning multiple result types
The @UnionValue type returns more than one entity type from a single query — here albums plus nearby concerts — encouraging you to derive related content rather than only matching pixels.
- 12:56 - Continuing search in your app
The semanticContentSearch schema lets people continue into your full in-app search — pre-populating results from the captured context so they land on filters, categories, and deeper content.
- 14:27 - System store integrations
Visual Intelligence can also write data your app reads back via system stores: events through EventKit (EKEventStore), contacts via CNContactStore, and medical-device readings via HealthKit (HKHealthStore). Observe store-change notifications so captured data appears automatically.
- 17:16 - Next steps
Recaps the two integration points, Image Search and system stores, across iOS, iPadOS, and macOS. Points to documentation and related App Intents and Vision sessions.