skills/apple-intelligence/visual-intelligence/SKILL.md
Integrate your app with iOS Visual Intelligence for camera-based search and object recognition. Use when adding visual search capabilities.
npx skillsauth add rshankras/claude-code-apple-skills visual-intelligenceInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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 contentimport VisualIntelligence
import AppIntents
struct 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)")
}
}
struct 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:
func 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:
func 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.@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.// ❌ Full-res image URLs in DisplayRepresentation for multi-result responses
// ✅ Thumbnail-sized images (two-column sheet); full-width only when returning one result
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(
title: "\(name)",
subtitle: "\(description)",
image: .init(named: thumbnailName)
)
}
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(
title: "\(name)",
subtitle: "\(category)",
image: .init(systemName: "tag.fill")
)
}
var 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.// ❌ 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.// ❌ Heavy loading inside OpenIntent.perform (runs during foregrounding)
// ✅ Navigate only; load after the view appears; reuse one OpenIntent everywhere
struct ProductEntity: AppEntity {
// ... other properties
var appLinkURL: URL? {
URL(string: "myapp://product/\(id)")
}
}
@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.
struct 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:
@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.
import 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 }
}
}
func values(for input: SemanticContentDescriptor) async throws -> [ProductEntity] {
// Limit results for quick response
let results = await search(input.labels)
return Array(results.prefix(15))
}
func 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 }
}
var 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)development
US web checkout via the StoreKit External Purchase Link entitlement — currently 0% Apple commission (litigation ongoing), how to ship it safely, and how to architect for a commission flip so a future ruling is a config change, not a rewrite. Use when adding external purchase links, weighing web checkout vs IAP, or planning US-storefront pricing strategy.
tools
Revenue beyond the single-app price tag — own-app bundles, Family Sharing as a conversion lever, cross-developer bundles & suites, and institutional licensing via Group Purchases / Apple School & Business Manager. Use when a developer has multiple apps, a subscription worth sharing, complementary indie partners, or school/clinic/business buyers.
testing
Run a structured accessibility audit on an iOS/macOS app — automated XCUITest audits, Accessibility Inspector, manual VoiceOver/Dynamic Type passes, and App Store Accessibility Nutrition Label evaluation. Use before release, when preparing Nutrition Label declarations, or for EU Accessibility Act compliance.
tools
Stage-by-stage audit of an app's App Store growth machinery against a 54-item P0–P9 playbook — every item scored from an App Store Connect MCP call, a codebase check, or an explicit question to the user, then routed to the skill or command that fixes it. Read-only on App Store Connect. Use for a growth audit or scorecard, a pre-launch growth plan, a quarterly re-audit, or "which growth levers am I missing."