关于 axiom-core-spotlight-ref
为 Apple 应用开发者提供 Core Spotlight 和 NSUserActivity 参考,区分内容索引与当前活动记录,并介绍 Spotlight 搜索、Siri 预测、Handoff 和 App Intents。
主要功能
- 01使用 CSSearchableItem 为内容建索引
- 02使用 NSUserActivity 记录活动
- 03区分内容索引与活动 API
- 04批量索引并删除已索引项目
- 05将活动关联到 App Intents
适用场景
- 01让应用文档或消息可在 Spotlight 中搜索
- 02支持从当前查看的界面使用 Handoff
- 03为应用活动启用 Siri 预测
- 04处理 Spotlight 搜索结果并删除索引项目
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ charleswiltgen/ axiom --skill axiom-core-spotlight-ref安装后在 Claude Code 中输入/axiom-core-spotlight-ref确认已加载。
详细介绍
Core Spotlight 与 NSUserActivity 参考
概述
Core Spotlight 框架与 NSUserActivity 综合指南:让应用内容可通过 Spotlight 搜索发现、启用 Siri 预测并支持 Handoff。Core Spotlight 直接为应用内容编制索引,而 NSUserActivity 则捕获用户参与情况以供预测。
主要区别 Core Spotlight = 为所有应用内容编制索引;NSUserActivity = 标记当前用户活动以供预测/Handoff。
何时使用此技能
在以下情况下使用此技能:
- 为 Spotlight 中的应用内容(文档、笔记、订单、消息)编制索引
- 使用 NSUserActivity 实现 Handoff 或 Siri 预测
- 在 CSSearchableItem、IndexedEntity 和 NSUserActivity 之间进行选择
- 实现从 Spotlight 搜索结果继续活动
- 为提升性能进行批量索引
- 删除已编制索引的内容
- 调试 Spotlight 搜索找不到应用内容的问题
- 将 NSUserActivity 与 App Intents 集成(appEntityIdentifier)
请勿在以下情况下使用此技能:
- 实现 App Shortcuts(使用 app-shortcuts-ref)
- 了解 App Intents 基础知识(使用 app-intents-ref)
- 制定整体可发现性策略(使用 app-discoverability)
相关技能
- app-intents-ref — App Intents 框架,包括 IndexedEntity
- app-discoverability — 提升应用可发现性的策略指南
- app-shortcuts-ref — 用于即时提供功能的 App Shortcuts
各 API 的适用场景
| 使用场景 | 方案 | 示例 |
|---|---|---|
| 用户正在查看特定屏幕 | NSUserActivity | 用户打开了订单详情 |
| 为所有应用内容编制索引 | CSSearchableItem | 可搜索全部 500 个订单 |
| 搜索 App Intents 实体 | IndexedEntity | “查找符合条件的订单……” |
| 在设备之间使用 Handoff | NSUserActivity | 在 Mac 上继续编辑笔记 |
| 在后台为内容编制索引 | CSSearchableItem 批量索引 | 启动时为文档编制索引 |
Apple 指南 将 NSUserActivity 用于用户主动发起的活动(当前显示的屏幕),而不要将其用作通用索引机制。若要为内容全面编制索引,请使用 Core Spotlight 的 CSSearchableItem。
Core Spotlight(CSSearchableItem)
创建可搜索项目
import CoreSpotlight
import UniformTypeIdentifiers
func indexOrder(_ order: Order) {
// 1. Create attribute set with metadata
let attributes = CSSearchableItemAttributeSet(contentType: .item)
attributes.title = order.coffeeName
attributes.contentDescription = "Ordered on \(order.date.formatted())"
attributes.keywords = ["coffee", "order", order.coffeeName.lowercased()]
attributes.thumbnailData = order.imageData
// Optional: Add location
attributes.latitude = order.location.coordinate.latitude
attributes.longitude = order.location.coordinate.longitude
// Optional: Add rating
attributes.rating = NSNumber(value: order.rating)
// 2. Create searchable item
let item = CSSearchableItem(
uniqueIdentifier: order.id.uuidString, // Stable ID
domainIdentifier: "orders", // Grouping
attributeSet: attributes
)
// Optional: Set expiration
item.expirationDate = Date().addingTimeInterval(60 * 60 * 24 * 365) // 1 year
// 3. Index the item
CSSearchableIndex.default().indexSearchableItems([item]) { error in
if let error = error {
print("Indexing error: \(error.localizedDescription)")
}
}
}
关键属性
uniqueIdentifier
用途 在应用内唯一标识此项目的稳定、持久 ID。
uniqueIdentifier: order.id.uuidString
要求:
- 必须稳定(同一项目 = 同一标识符)
- 用于更新和删除
- 作用范围限定为你的应用
domainIdentifier
用途 将相关项目分组,以便执行批量操作。
domainIdentifier: "orders"
适用场景:
- 删除某个域中的所有项目
- 按类型组织(订单、文档、消息)
- 批量操作
模式:
// Index with domains
item1.domainIdentifier = "orders"
item2.domainIdentifier = "documents"
// Delete entire domain
CSSearchableIndex.default().deleteSearchableItems(
withDomainIdentifiers: ["orders"]
) { error in }
CSSearchableItemAttributeSet
描述可搜索内容的元数据。
let attributes = CSSearchableItemAttributeSet(contentType: .item)
// Required
attributes.title = "Order #1234"
attributes.displayName = "Coffee Order"
// Highly recommended
attributes.contentDescription = "Medium latte with oat milk"
attributes.keywords = ["coffee", "latte", "order"]
attributes.thumbnailData = imageData
// Optional but valuable
attributes.contentCreationDate = Date()
attributes.contentModificationDate = Date()
attributes.rating = NSNumber(value: 5)
attributes.comment = "My favorite order"
常用属性
| 属性 | 用途 | 示例 |
|---|---|---|
title | 主标题 | “Coffee Order #1234” |
displayName | 用户可见名称 | “Morning Latte” |
contentDescription | 描述文本 | “中杯燕麦奶拿铁” |
keywords | 搜索词 | [“coffee”, “latte”] |
thumbnailData | 预览图像 | JPEG/PNG 数据 |
contentCreationDate | 创建时间 | Date() |
contentModificationDate | 上次修改时间 | Date() |
rating | 星级评分 | NSNumber(value: 5) |
latitude / longitude | 位置 | 37.7749, -122.4194 |
文档专用属性
// For document types
attributes.contentType = UTType.pdf
attributes.author = "John Doe"
attributes.pageCount = 10
attributes.fileSize = 1024000
attributes.path = "/path/to/document.pdf"
消息专用属性
// For messages
attributes.recipients = ["[email protected]"]
attributes.recipientNames = ["Jane Doe"]
attributes.authorNames = ["John Doe"]
attributes.subject = "Meeting notes"
批量索引以提升性能
❌ 不要:逐个为项目编制索引
// Bad: 100 index operations
for order in orders {
CSSearchableIndex.default().indexSearchableItems([order.asSearchableItem()]) { _ in }
}
✅ 应当:批量执行索引操作
// Good: 1 index operation
let items = orders.map { $0.asSearchableItem() }
CSSearchableIndex.default().indexSearchableItems(items) { error in
if let error = error {
print("Batch indexing error: \(error)")
} else {
print("Indexed \(items.count) items")
}
}
建议的批次大小 每次调用处理 100-500 个项目。对于更大的集合,请拆分成多个批次。
删除模式
按标识符删除
let identifiers = ["order-1", "order-2", "order-3"]
CSSearchableIndex.default().deleteSearchableItems(
withIdentifiers: identifiers
) { error in
if let error = error {
print("Deletion error: \(error)")
}
}
按域删除
// Delete all items in "orders" domain
CSSearchableIndex.default().deleteSearchableItems(
withDomainIdentifiers: ["orders"]
) { error in }
全部删除
// Nuclear option: delete everything
CSSearchableIndex.default().deleteAllSearchableItems { error in
if let error = error {
print("Failed to delete all: \(error)")
}
}
删除时机:
- 用户删除内容
- 内容过期
- 用户退出登录
- 应用重置/重新安装
应用实体集成(App Intents)
从应用实体创建
import AppIntents
struct OrderEntity: AppEntity, IndexedEntity {
var id: UUID
@Property(title: "Coffee", indexingKey: \.title)
var coffeeName: String
@Property(title: "Date", indexingKey: \.contentCreationDate)
var orderDate: Date
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Order"
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: "\(coffeeName)", subtitle: "Order from \(orderDate.formatted())")
}
}
// Create searchable item from entity
let order = OrderEntity(id: UUID(), coffeeName: "Latte", orderDate: Date())
let item = CSSearchableItem(appEntity: order)
CSSearchableIndex.default().indexSearchableItems([item])
将实体与现有项目关联
let attributes = CSSearchableItemAttributeSet(contentType: .item)
attributes.title = "Order #1234"
let item = CSSearchableItem(
uniqueIdentifier: "order-1234",
domainIdentifier: "orders",
attributeSet: attributes
)
// Associate with App Intent entity
item.associateAppEntity(orderEntity, priority: .default)
优点:
- 在“快捷指令”中自动执行“查找”操作
- Spotlight 搜索直接返回实体
- 集成 App Intents
NSUserActivity
概述
NSUserActivity 用于捕获用户参与情况,支持:
- Handoff — 在另一台设备上继续活动
- Spotlight 搜索 — 为当前查看的内容编制索引
- Siri 预测 — 建议返回此屏幕
- 快速备忘录 — 将备忘录关联到应用内容
平台支持 iOS 8.0+、iPadOS 8.0+、macOS 10.10+、tvOS 9.0+、watchOS 2.0+、axiom-visionOS 1.0+
资格属性
let activity = NSUserActivity(activityType: "com.app.viewOrder")
// Enable Spotlight search
activity.isEligibleForSearch = true
// Enable Siri predictions
activity.isEligibleForPrediction = true
// Enable Handoff to other devices
activity.isEligibleForHandoff = true
// Contribute URL to global search (public content only)
activity.isEligibleForPublicIndexing = false
隐私提示 仅对可公开访问的内容(例如具有公开 URL 的博客文章)设置 isEligibleForPublicIndexing = true。
基本模式
func viewOrder(_ order: Order) {
// 1. Create activity
let activity = NSUserActivity(activityType: "com.coffeeapp.viewOrder")
activity.title = order.coffeeName
// 2. Set eligibility
activity.isEligibleForSearch = true
activity.isEligibleForPrediction = true
// 3. Provide identifier for updates/deletion
activity.persistentIdentifier = order.id.uuidString
// 4. Provide rich metadata
let attributes = CSSearchableItemAttributeSet(contentType: .item)
attributes.title = order.coffeeName
attributes.contentDescription = "Your \(order.coffeeName) order"
attributes.thumbnailData = order.imageData
activity.contentAttributeSet = attributes
// 5. Mark as current
activity.becomeCurrent()
// 6. Store reference (important!)
self.userActivity = activity
}
关键 始终保留对 activity 的强引用。否则它不会出现在搜索结果中。
becomeCurrent() 和 resignCurrent()
// UIKit pattern
class OrderDetailViewController: UIViewController {
var currentActivity: NSUserActivity?
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let activity = NSUserActivity(activityType: "com.app.viewOrder")
activity.title = order.coffeeName
activity.isEligibleForSearch = true
activity.becomeCurrent() // Mark as active
self.currentActivity = activity
self.userActivity = activity // UIKit integration
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
currentActivity?.resignCurrent() // Mark as inactive
}
}
// SwiftUI pattern
struct OrderDetailView: View {
let order: Order
var body: some View {
VStack {
Text(order.coffeeName)
}
.onAppear {
let activity = NSUserActivity(activityType: "com.app.viewOrder")
activity.title = order.coffeeName
activity.isEligibleForSearch = true
activity.becomeCurrent()
// SwiftUI automatically manages userActivity
self.userActivity = activity
}
}
}
App Intents 集成(appEntityIdentifier)
将 NSUserActivity 与 App Intent 实体关联。
func viewOrder(_ order: Order) {
let activity = NSUserActivity(activityType: "com.app.viewOrder")
activity.title = order.coffeeName
activity.isEligibleForSearch = true
activity.isEligibleForPrediction = true
// Connect to App Intent entity
activity.appEntityIdentifier = order.id.uuidString
// Now Spotlight can surface this as an entity suggestion
activity.becomeCurrent()
self.userActivity = activity
}
优点:
- Siri 会在相关场景中建议此订单
- App Intents 可以引用此 activity
- 集成“快捷指令”
屏幕内容标记
WWDC 中的模式 为当前可见内容添加标记,以便 Spotlight 提供参数建议。
func showEvent(_ event: Event) {
let activity = NSUserActivity(activityType: "com.app.viewEvent")
activity.persistentIdentifier = event.id.uuidString
// Spotlight suggests this event for intent parameters
activity.appEntityIdentifier = event.id.uuidString
activity.becomeCurrent()
userActivity = activity
}
结果 当用户调用需要事件参数的 intent 时,Spotlight 会建议当前可见的事件。
快速备忘录集成(macOS/iPadOS)
要支持快速备忘录链接,activity 必须:
- 是应用的当前 activity(通过
becomeCurrent()) - 具有清晰、简洁的
title(使用名词,而非动词) - 提供稳定且一致的标识符
- 始终支持导航到关联内容
- 妥善处理内容缺失的情况
let activity = NSUserActivity(activityType: "com.app.viewNote")
activity.title = note.title // ✅ "Project Ideas" not ❌ "View Note"
activity.persistentIdentifier = note.id.uuidString
activity.targetContentIdentifier = note.id.uuidString
activity.becomeCurrent()
活动延续(处理 Spotlight 点按)
用户点按 Spotlight 结果时,处理活动延续:
UIKit
// AppDelegate or SceneDelegate
func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
guard userActivity.activityType == "com.app.viewOrder" else {
return false
}
// Extract identifier
if let identifier = userActivity.persistentIdentifier,
let orderID = UUID(uuidString: identifier) {
// Navigate to order
navigateToOrder(orderID)
return true
}
return false
}
SwiftUI
@main
struct CoffeeApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.onContinueUserActivity("com.app.viewOrder") { userActivity in
if let identifier = userActivity.persistentIdentifier,
let orderID = UUID(uuidString: identifier) {
// Navigate to order
navigateToOrder(orderID)
}
}
}
}
}
可搜索项目延续
// When continuing from CSSearchableItem
func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
if userActivity.activityType == CSSearchableItemActionType {
// Get identifier from Core Spotlight item
if let identifier = userActivity.userInfo?[CSSearchableItemActivityIdentifier] as? String {
// Navigate based on identifier
navigateToItem(identifier)
return true
}
}
return false
}
删除 API
删除所有已保存的活动
NSUserActivity.deleteAllSavedUserActivities { }
删除指定活动
let identifiers = ["order-1", "order-2"]
NSUserActivity.deleteSavedUserActivities(
withPersistentIdentifiers: identifiers
) { }
删除时机:
- 用户删除内容
- 用户退出登录
- 内容不再可访问
NSUserActivity 与 CSSearchableItem 对比
| 方面 | NSUserActivity | CSSearchableItem |
|---|---|---|
| 用途 | 当前用户活动 | 为所有内容编制索引 |
| 使用时机 | 用户正在查看某个屏幕 | 在后台为内容编制索引 |
| 范围 | 一次处理一个项目 | 批量操作 |
| Handoff | 支持 | 不支持 |
| 预测 | 支持 | 不支持 |
| 搜索 | 有限 | 完整集成 Spotlight |
| 示例 | 用户正在查看订单详情 | 为全部 500 个订单编制索引 |
建议 同时使用:
- 对当前可见的屏幕使用 NSUserActivity
- 使用 CSSearchableItem 为内容建立全面索引
测试与调试
验证已编制索引的项目
使用 Spotlight
- 打开 Spotlight(在主屏幕上向下轻扫)
- 搜索已编制索引内容中的关键词
- 确认应用结果是否显示
- 点按结果 → 确认导航是否正常
使用控制台日志
CSSearchableIndex.default().fetchLastClientState { clientState, error in
if let error = error {
print("Error fetching client state: \(error)")
} else {
print("Client state: \(clientState?.base64EncodedString() ?? "none")")
}
}
常见问题
项目未出现在 Spotlight 中
- 等待 1–2 分钟,让系统完成索引
- 确认
isEligibleForSearch = true - 检查“系统设置” → “Siri 与搜索” → [App] → “在搜索中显示 App”
- 重新启动设备
- 检查控制台中是否有索引错误
活动未触发 Handoff
- 确认
isEligibleForHandoff = true - 确保两台设备均登录了同一个 iCloud 账户
- 检查两台设备的蓝牙和 Wi-Fi 是否已启用
- 确认 activityType 使用反向 DNS 格式(com.company.app.action)
延续功能无法正常工作
- 确认已实现
application(_:continue:restorationHandler:) - 检查 activityType 是否完全匹配
- 确保已设置 persistentIdentifier
- 使用调试器测试,确认方法是否被调用
最佳实践
1. 选择性编制索引
❌ 不要:为所有内容编制索引
// Bad: Index all 10,000 items
let allItems = try await ItemService.shared.all()
✅ 应当:有选择地建立索引
// Good: Index recent/important items
let recentItems = try await ItemService.shared.recent(limit: 100)
let favoriteItems = try await ItemService.shared.favorites()
**原因:**性能、配额限制和用户体验。
2. 使用领域标识符
❌ 不要:只依赖唯一标识符
// Hard to delete all orders
CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: allOrderIDs)
✅ 应当:按领域分组
// Easy to delete all orders
CSSearchableIndex.default().deleteSearchableItems(withDomainIdentifiers: ["orders"])
3. 设置过期日期
❌ 不要:让条目永不过期
// Bad: Items never expire
let item = CSSearchableItem(/* ... */)
✅ 应当:设置合理的过期时间
// Good: Expire after 1 year
item.expirationDate = Date().addingTimeInterval(60 * 60 * 24 * 365)
4. 提供丰富的元数据
❌ 不要:只提供最少的元数据
attributes.title = "Item"
✅ 应当:提供丰富且可搜索的元数据
attributes.title = "Medium Latte Order"
attributes.contentDescription = "Ordered on December 12, 2025"
attributes.keywords = ["coffee", "latte", "order", "medium"]
attributes.thumbnailData = imageData
5. 妥善处理缺失的内容
func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
guard let identifier = userActivity.persistentIdentifier else {
return false
}
// Attempt to load content
if let item = try? await ItemService.shared.fetch(id: identifier) {
navigate(to: item)
return true
} else {
// Content deleted or unavailable
showAlert("This content is no longer available")
// Delete activity from search
NSUserActivity.deleteSavedUserActivities(
withPersistentIdentifiers: [identifier]
)
return true // Still handled
}
}
完整示例
全面集成
import CoreSpotlight
import UniformTypeIdentifiers
class OrderManager {
// MARK: - Core Spotlight Indexing
func indexOrder(_ order: Order) {
let attributes = CSSearchableItemAttributeSet(contentType: .item)
attributes.title = order.coffeeName
attributes.contentDescription = "Order from \(order.date.formatted())"
attributes.keywords = ["coffee", "order", order.coffeeName.lowercased()]
attributes.thumbnailData = order.thumbnailImageData
attributes.contentCreationDate = order.date
attributes.rating = NSNumber(value: order.rating)
let item = CSSearchableItem(
uniqueIdentifier: order.id.uuidString,
domainIdentifier: "orders",
attributeSet: attributes
)
item.expirationDate = Date().addingTimeInterval(60 * 60 * 24 * 365)
CSSearchableIndex.default().indexSearchableItems([item]) { error in
if let error = error {
print("Indexing error: \(error)")
}
}
}
func deleteOrder(_ orderID: UUID) {
// Delete from Core Spotlight
CSSearchableIndex.default().deleteSearchableItems(
withIdentifiers: [orderID.uuidString]
)
// Delete NSUserActivity
NSUserActivity.deleteSavedUserActivities(
withPersistentIdentifiers: [orderID.uuidString]
)
}
func deleteAllOrders() {
CSSearchableIndex.default().deleteSearchableItems(
withDomainIdentifiers: ["orders"]
)
}
// MARK: - NSUserActivity for Current Screen
func createActivityForOrder(_ order: Order) -> NSUserActivity {
let activity = NSUserActivity(activityType: "com.coffeeapp.viewOrder")
activity.title = order.coffeeName
activity.isEligibleForSearch = true
activity.isEligibleForPrediction = true
activity.persistentIdentifier = order.id.uuidString
// Connect to App Intents
activity.appEntityIdentifier = order.id.uuidString
// Rich metadata
let attributes = CSSearchableItemAttributeSet(contentType: .item)
attributes.title = order.coffeeName
attributes.contentDescription = "Your \(order.coffeeName) order"
attributes.thumbnailData = order.thumbnailImageData
activity.contentAttributeSet = attributes
return activity
}
}
// UIKit view controller
class OrderDetailViewController: UIViewController {
var order: Order!
var currentActivity: NSUserActivity?
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
currentActivity = OrderManager.shared.createActivityForOrder(order)
currentActivity?.becomeCurrent()
self.userActivity = currentActivity
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
currentActivity?.resignCurrent()
}
}
// SwiftUI view
struct OrderDetailView: View {
let order: Order
var body: some View {
VStack {
Text(order.coffeeName)
.font(.largeTitle)
Text("Ordered on \(order.date.formatted())")
.foregroundColor(.secondary)
}
.userActivity("com.coffeeapp.viewOrder") { activity in
activity.title = order.coffeeName
activity.isEligibleForSearch = true
activity.isEligibleForPrediction = true
activity.persistentIdentifier = order.id.uuidString
activity.appEntityIdentifier = order.id.uuidString
let attributes = CSSearchableItemAttributeSet(contentType: .item)
attributes.title = order.coffeeName
attributes.contentDescription = "Your \(order.coffeeName) order"
activity.contentAttributeSet = attributes
}
}
}
资源
WWDC:260、2015-709
文档:/corespotlight、/corespotlight/cssearchableitem、/foundation/nsuseractivity
技能:axiom-app-intents-ref、axiom-app-discoverability、axiom-app-shortcuts-ref
**请记住:**Core Spotlight 会为应用中的所有内容建立索引;NSUserActivity 标记用户当前正在进行的操作。使用 CSSearchableItem 批量建立索引,使用 NSUserActivity 标记当前活动屏幕,并通过 appEntityIdentifier 将它们与 App Intents 关联,以实现全面的可发现性。