-
앱에 시각 지능을 통합하기 위한 모범 사례
시각 지능으로 어떻게 앱 내 콘텐츠 탐색을 혁신할 수 있는지 인사이트를 얻어 보세요. 엔티티를 정의하고, 이미지를 처리하며, 다양한 결과 유형을 효과적으로 처리하는 방법을 살펴보세요. 속도와 관련성을 최적화하는 모범 사례를 알아보고, 인텐트가 탭 한 번으로 콘텐츠 열기 또는 재생하기 같은 직접적인 동작을 어떻게 구현하는지 살펴보세요.
챕터
- 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
리소스
-
비디오 검색…
-
-
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) } } } --- snippet. -
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.