Visual Intelligence Image Search lets apps surface their content directly in the system's Visual Intelligence results sheet when users capture images or screenshots. Apps define App Entities, implement an IntentValueQuery with a SemanticContentDescriptor input, and provide an OpenIntent to deep-link into the app.
โข Apps can appear alongside other providers in Visual Intelligence results when users highlight-to-search, dramatically increasing discoverability without any user intent to open the app.
โข The same entity, query, and OpenIntent code works across iOS, iPadOS, and macOS with minimal changes โ one integration reaches all three platforms.
โข System store integrations (EventKit, Contacts, HealthKit) allow apps to passively receive data extracted by Visual Intelligence, turning camera captures into structured in-app data.
Demonstrates how to define an App Entity for albums, implement an IntentValueQuery that accepts a SemanticContentDescriptor, and use Vision's GenerateImageFeaturePrintRequest to return visually similar results to Visual Intelligence.
import AppIntents
import VisualIntelligence
import Vision
import UIKit
import VideoToolbox
// MARK: - App Entity
struct AlbumEntity: AppEntity {
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Album"
static var defaultQuery = AlbumEntityQuery()
var id: String
var name: String
var artistName: String
var artworkURL: URL?
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(
title: "\(name)",
subtitle: "\(artistName)",
image: artworkURL.map { .init(url: $0) }
)
}
}
// MARK: - Basic EntityQuery (required by AppEntity)
struct AlbumEntityQuery: EntityQuery {
func entities(for identifiers: [AlbumEntity.ID]) async throws -> [AlbumEntity] {
AlbumCatalog.shared.albums(for: identifiers)
}
}
// MARK: - Image Search Query
struct AlbumImageSearchQuery: IntentValueQuery {
typealias Value = AlbumEntity
func values(for requirement: SemanticContentDescriptor) async throws -> [AlbumEntity] {
guard let pixelBuffer = requirement.pixelBuffer else { return [] }
// Convert CVPixelBuffer โ CGImage
var cgImage: CGImage?
VTCreateCGImageFromCVPixelBuffer(pixelBuffer, options: nil, imageOut: &cgImage)
guard let image = cgImage else { return [] }
// Generate a feature print for the query image
let request = GenerateImageFeaturePrintRequest()
let handler = ImageRequestHandler(image)
let result = try handler.perform(request)
let queryPrint = result.featurePrint
// Compare against pre-computed catalog feature prints
let matches = AlbumCatalog.shared.allEntries.compactMap { entry -> (AlbumEntity, Float)? in
var distance: Float = 0
try? queryPrint.computeDistance(&distance, to: entry.featurePrint)
return distance < 0.6 ? (entry.entity, distance) : nil
}
return matches
.sorted { $0.1 < $1.1 } // closest first
.prefix(5)
.map(\.0)
}
}
// MARK: - Open Intent
struct OpenAlbumIntent: OpenIntent {
static var title: LocalizedStringResource = "Open Album"
@Parameter(title: "Album") var target: AlbumEntity
@MainActor
func perform() async throws -> some IntentResult {
AppNavigator.shared.navigate(to: .album(id: target.id))
return .result()
}
}
// MARK: - Catalog stub (replace with your real data layer)
struct CatalogEntry {
let entity: AlbumEntity
let featurePrint: VNFeaturePrintObservation
}
final class AlbumCatalog {
static let shared = AlbumCatalog()
var allEntries: [CatalogEntry] = []
func albums(for ids: [String]) -> [AlbumEntity] {
allEntries.filter { ids.contains($0.entity.id) }.map(\.entity)
}
}
// MARK: - Navigation stub
final class AppNavigator {
static let shared = AppNavigator()
enum Destination { case album(id: String) }
func navigate(to destination: Destination) { /* drive your NavigationStack */ }
}You can only have one IntentValueQuery that accepts a SemanticContentDescriptor per app; use @UnionValue to return multiple entity types from that single query. Pre-compute feature prints for your catalog at build/launch time โ not at query time โ to meet latency expectations. Keep OpenIntent.perform lightweight since it runs as the app transitions to foreground. Return an empty array (not an error) when no good matches are found.
Requires Apple Intelligence-capable device; also available on iPadOS 27+ and macOS 26+. On macOS, input pixel buffers can be significantly larger than on iPhone โ consider resizing before feature-print generation.
More iOS 27 APIs land every week.
Get notified when new capabilities are published โ no noise, just signal.