Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Integrate your app with iOS Visual Intelligence for camera-based search and object recognition. Use when adding visual search capabilities.
.claude/skills/rshankras-visual-intelligence/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 102% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 165% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 125% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 162% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 114% | 0% |
Integrate your app with iOS Visual Intelligence to let users find app content by pointing their camera at objects.
Visual Intelligence lets users:
Your app implements:
IntentValueQuery to receive search requestsAppEntity types for searchable contentswiftimport VisualIntelligence import AppIntents
swiftstruct ProductEntity: AppEntity { var id: String var name: String var price: String var imageName: String static var typeDisplayRepresentation: TypeDisplayRepresentation { TypeDisplayRepresentation( name: LocalizedStringResource("Product"), numericFormat: "\(placeholder: .int) products" ) } var displayRepresentation: DisplayRepresentation { DisplayRepresentation( title: "\(name)", subtitle: "\(price)", image: .init(named: imageName) ) } // Deep link URL var appLinkURL: URL? { URL(string: "myapp://product/\(id)") } }
swiftstruct ProductIntentValueQuery: IntentValueQuery { func values(for input: SemanticContentDescriptor) async throws -> [ProductEntity] { // Search using labels if !input.labels.isEmpty { return await searchProducts(matching: input.labels) } // Search using image if let pixelBuffer = input.pixelBuffer { return await searchProducts(from: pixelBuffer) } return [] } private func searchProducts(matching labels: [String]) async -> [ProductEntity] { // Search your database using provided labels // Return matching products } private func searchProducts(from pixelBuffer: CVReadOnlyPixelBuffer) async -> [ProductEntity] { // Use image recognition on the pixel buffer // Return matching products } }
The system provides this object with information about what the user is looking at.
| Property | Type | Description | |----------|------|-------------| | labels | [String] | Classification labels from Visual Intelligence | | pixelBuffer | CVReadOnlyPixelBuffer? | Raw image data |
Label-based Search:
swiftfunc values(for input: SemanticContentDescriptor) async throws -> [ProductEntity] { // Labels like "shoe", "sneaker", "Nike" etc. let labels = input.labels // Search your content using these labels return products.filter { product in labels.contains { label in product.tags.contains(label.lowercased()) } } }
Image-based Search:
swiftfunc values(for input: SemanticContentDescriptor) async throws -> [ProductEntity] { guard let pixelBuffer = input.pixelBuffer else { return [] } // Convert to CGImage for processing let ciImage = CIImage(cvPixelBuffer: pixelBuffer) let context = CIContext() guard let cgImage = context.createCGImage(ciImage, from: ciImage.extent) else { return [] } // Use your ML model or image matching logic return await imageSearch.findMatches(for: cgImage) }
Use @UnionValue when your app has different content types.
Rules (WWDC26 297):
IntentValueQuery that accepts a SemanticContentDescriptor. All result types must flow through that single query — a @UnionValue enum with one case per entity type.OpenIntent — without one, results of that type can't appear in image search.swift@UnionValue enum SearchResult { case product(ProductEntity) case category(CategoryEntity) case store(StoreEntity) } struct VisualSearchQuery: IntentValueQuery { func values(for input: SemanticContentDescriptor) async throws -> [SearchResult] { var results: [SearchResult] = [] // Search products let products = await productSearch(input.labels) results.append(contentsOf: products.map { .product($0) }) // Search categories let categories = await categorySearch(input.labels) results.append(contentsOf: categories.map { .category($0) }) return results } }
Create compelling visual representations for search results.
DisplayRepresentation with an image URL, serve a thumbnail-sized image, not the full-resolution asset — smaller images load faster.swift// ❌ Full-res image URLs in DisplayRepresentation for multi-result responses // ✅ Thumbnail-sized images (two-column sheet); full-width only when returning one result
swiftvar displayRepresentation: DisplayRepresentation { DisplayRepresentation( title: "\(name)", subtitle: "\(description)", image: .init(named: thumbnailName) ) }
swiftvar displayRepresentation: DisplayRepresentation { DisplayRepresentation( title: "\(name)", subtitle: "\(category)", image: .init(systemName: "tag.fill") ) }
swiftvar displayRepresentation: DisplayRepresentation { DisplayRepresentation( title: LocalizedStringResource("\(name)"), subtitle: LocalizedStringResource("\(formatPrice(price))"), image: DisplayRepresentation.Image(named: imageName) ) }
Whether you're searching on device or hitting a server, the same principles apply: return results fast and ranked.
Vision-framework pattern:
GenerateImageFeaturePrintRequest feature prints for your catalog; at query time, convert the pixel buffer via VideoToolbox (VTCreateCGImageFromCVPixelBuffer) and generate just one feature print to compare.search(matching:limit: Int = 10, maxDistance: Double = 1.0).[] when nothing matches or the pixel buffer is absent — the system handles displaying an empty response. ❌ Don't pad with weak matches.swift// ❌ Compute feature prints at query time // ✅ Pre-compute catalog prints; query = 1 print + threshold + sort + limit let matches = catalogPrints .map { entry in (entry, entry.print.distance(to: queryPrint)) } .filter { $0.1 <= maxDistance } .sorted { $0.1 < $1.1 } .prefix(limit)
Vision offers more than feature prints for visual search: text extraction, barcode scanning, face detection, image classification (WWDC26 297).
Enable users to open specific content from search results.
Tapping a result runs your OpenIntent for that entity type, and its perform() runs as the app comes to the foreground:
perform(); defer heavy loading until after the view appears.swift// ❌ Heavy loading inside OpenIntent.perform (runs during foregrounding) // ✅ Navigate only; load after the view appears; reuse one OpenIntent everywhere
swiftstruct ProductEntity: AppEntity { // ... other properties var appLinkURL: URL? { URL(string: "myapp://product/\(id)") } }
swift@main struct MyApp: App { var body: some Scene { WindowGroup { ContentView() .onOpenURL { url in handleDeepLink(url) } } } func handleDeepLink(_ url: URL) { guard url.scheme == "myapp" else { return } switch url.host { case "product": let id = url.lastPathComponent navigationState.showProduct(id: id) default: break } } }
Provide access to additional results beyond the initial set.
swiftstruct ViewMoreProductsIntent: AppIntent, VisualIntelligenceSearchIntent { static var title: LocalizedStringResource = "View More Products" @Parameter(title: "Semantic Content") var semanticContent: SemanticContentDescriptor func perform() async throws -> some IntentResult { // Store search context for your app SearchContext.shared.currentSearch = semanticContent.labels // Return empty result - system will open your app return .result() } }
The schema-based form: conform to .visualIntelligence.semanticContentSearch and the system supplies the semanticContent property automatically:
swift@AppIntent(schema: .visualIntelligence.semanticContentSearch) struct SemanticContentSearchIntent: AppIntent { static let openAppWhenRun: Bool = true var semanticContent: SemanticContentDescriptor func perform() async throws -> some IntentResult { let results = try await library.search(matching: semanticContent) await MainActor.run { AppState.shared.openSearch(with: results) } return .result() } }
Rules (WWDC26 297): pre-populate the in-app search view from the captured context (never a blank screen), and use it to expose what the Visual Intelligence sheet can't — filters, categories, the full depth of your content.
swiftimport SwiftUI import AppIntents import VisualIntelligence // MARK: - Entities struct RecipeEntity: AppEntity { var id: String var name: String var cuisine: String var prepTime: String var imageName: String static var typeDisplayRepresentation: TypeDisplayRepresentation { TypeDisplayRepresentation( name: LocalizedStringResource("Recipe"), numericFormat: "\(placeholder: .int) recipes" ) } var displayRepresentation: DisplayRepresentation { DisplayRepresentation( title: "\(name)", subtitle: "\(cuisine) · \(prepTime)", image: .init(named: imageName) ) } var appLinkURL: URL? { URL(string: "recipes://recipe/\(id)") } } // MARK: - Intent Value Query struct RecipeVisualSearchQuery: IntentValueQuery { @Dependency var recipeStore: RecipeStore func values(for input: SemanticContentDescriptor) async throws -> [RecipeEntity] { // Use labels to find recipes // Labels might include: "pasta", "tomato", "Italian", etc. let matchingRecipes = await recipeStore.search( ingredients: input.labels, limit: 15 ) return matchingRecipes.map { recipe in RecipeEntity( id: recipe.id, name: recipe.name, cuisine: recipe.cuisine, prepTime: recipe.prepTimeFormatted, imageName: recipe.thumbnailName ) } } } // MARK: - More Results Intent struct ViewMoreRecipesIntent: AppIntent, VisualIntelligenceSearchIntent { static var title: LocalizedStringResource = "View More Recipes" @Parameter(title: "Semantic Content") var semanticContent: SemanticContentDescriptor func perform() async throws -> some IntentResult { // Save search context await MainActor.run { RecipeSearchState.shared.searchTerms = semanticContent.labels } return .result() } } // MARK: - Recipe Store @Observable class RecipeStore { private var recipes: [Recipe] = [] func search(ingredients: [String], limit: Int) async -> [Recipe] { recipes .filter { recipe in ingredients.contains { ingredient in recipe.ingredients.contains { recipeIngredient in recipeIngredient.lowercased().contains(ingredient.lowercased()) } } } .prefix(limit) .map { $0 } } }
swiftfunc values(for input: SemanticContentDescriptor) async throws -> [ProductEntity] { // Limit results for quick response let results = await search(input.labels) return Array(results.prefix(15)) }
swiftfunc values(for input: SemanticContentDescriptor) async throws -> [ProductEntity] { let results = await search(input.labels) // Sort by relevance score return results .filter { $0.relevanceScore > 0.5 } .sorted { $0.relevanceScore > $1.relevanceScore } .prefix(15) .map { $0 } }
swiftvar displayRepresentation: DisplayRepresentation { DisplayRepresentation( title: LocalizedStringResource(stringLiteral: name), subtitle: LocalizedStringResource( stringLiteral: "\(category) · \(formattedPrice)" ), image: .init(named: thumbnailName) ) }
Two integration directions: your app provides results (everything above), and your app receives data Visual Intelligence writes into shared system stores:
| Visual Intelligence system action | Store | Your app reads via | |-----------------------------------|-------|--------------------| | Create calendar events — including multiple events at once | EventKit | EKEventStore | | Add to contacts | Contacts | CNContactStore | | Log medical device readings (blood pressure monitors, glucose meters, weight scales) | HealthKit | HKHealthStore |
If your app already reads from these stores, Visual Intelligence becomes a source of input automatically — zero VI-specific code. One requirement: observe change notifications so VI-created data appears without a relaunch. EventKit pattern from Apple's sample: requestFullAccessToEvents() → fetch with a predicate (a 90-day window) → observe .EKEventStoreChanged notifications and refetch.
perform(), heavy loading deferred (WWDC26 297)[] when nothing matches — no weak-match padding (WWDC26 297)Other measured skills in the registry, with their headline benchmark lift.