axiom-core-spotlight-ref by charleswiltgen/axiom
npx skills add https://github.com/charleswiltgen/axiom --skill axiom-core-spotlight-ref关于 Core Spotlight 框架和 NSUserActivity 的全面指南,用于使应用内容在 Spotlight 搜索中可被发现、启用 Siri 预测以及支持接力功能。Core Spotlight 直接索引应用内容,而 NSUserActivity 则捕获用户参与度以进行预测。
关键区别 Core Spotlight = 索引所有应用内容;NSUserActivity = 标记当前用户活动以进行预测/接力。
在以下情况下使用此技能:
请勿在以下情况下使用此技能:
| 使用场景 | 方法 | 示例 |
|---|
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
| 用户查看特定屏幕 | NSUserActivity | 用户打开了订单详情 |
| 索引所有应用内容 | CSSearchableItem | 所有 500 个订单可搜索 |
| App Intents 实体搜索 | IndexedEntity | "查找订单,其中..." |
| 设备间接力 | NSUserActivity | 在 Mac 上继续编辑笔记 |
| 后台内容索引 | CSSearchableItem 批量 | 启动时索引文档 |
苹果指南 将 NSUserActivity 用于用户发起的活动(当前可见的屏幕),而不是作为通用索引机制。对于全面的内容索引,请使用 Core Spotlight 的 CSSearchableItem。
import CoreSpotlight
import UniformTypeIdentifiers
func indexOrder(_ order: Order) {
// 1. 创建包含元数据的属性集
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
// 可选:添加位置
attributes.latitude = order.location.coordinate.latitude
attributes.longitude = order.location.coordinate.longitude
// 可选:添加评分
attributes.rating = NSNumber(value: order.rating)
// 2. 创建可搜索项
let item = CSSearchableItem(
uniqueIdentifier: order.id.uuidString, // 稳定 ID
domainIdentifier: "orders", // 分组
attributeSet: attributes
)
// 可选:设置过期时间
item.expirationDate = Date().addingTimeInterval(60 * 60 * 24 * 365) // 1 年
// 3. 索引该项
CSSearchableIndex.default().indexSearchableItems([item]) { error in
if let error = error {
print("Indexing error: \(error.localizedDescription)")
}
}
}
用途 在您的应用内,此项目的稳定、持久且唯一的 ID。
uniqueIdentifier: order.id.uuidString
要求:
用途 对相关项目进行分组,以便进行批量操作。
domainIdentifier: "orders"
使用场景:
模式:
// 使用域进行索引
item1.domainIdentifier = "orders"
item2.domainIdentifier = "documents"
// 删除整个域
CSSearchableIndex.default().deleteSearchableItems(
withDomainIdentifiers: ["orders"]
) { error in }
描述可搜索内容的元数据。
let attributes = CSSearchableItemAttributeSet(contentType: .item)
// 必需
attributes.title = "Order #1234"
attributes.displayName = "Coffee Order"
// 强烈推荐
attributes.contentDescription = "Medium latte with oat milk"
attributes.keywords = ["coffee", "latte", "order"]
attributes.thumbnailData = imageData
// 可选但很有价值
attributes.contentCreationDate = Date()
attributes.contentModificationDate = Date()
attributes.rating = NSNumber(value: 5)
attributes.comment = "My favorite order"
| 属性 | 用途 | 示例 |
|---|---|---|
title | 主标题 | "Coffee Order #1234" |
displayName | 用户可见的名称 | "Morning Latte" |
contentDescription | 描述文本 | "Medium latte with oat milk" |
keywords | 搜索词 | ["coffee", "latte"] |
thumbnailData | 预览图像 | JPEG/PNG 数据 |
contentCreationDate | 创建时间 | Date() |
contentModificationDate | 最后修改时间 | Date() |
rating | 星级评分 | NSNumber(value: 5) |
latitude / longitude | 位置 | 37.7749, -122.4194 |
// 对于文档类型
attributes.contentType = UTType.pdf
attributes.author = "John Doe"
attributes.pageCount = 10
attributes.fileSize = 1024000
attributes.path = "/path/to/document.pdf"
// 对于消息
attributes.recipients = ["jane@example.com"]
attributes.recipientNames = ["Jane Doe"]
attributes.authorNames = ["John Doe"]
attributes.subject = "Meeting notes"
// 不好:100 次索引操作
for order in orders {
CSSearchableIndex.default().indexSearchableItems([order.asSearchableItem()]) { _ in }
}
// 好:1 次索引操作
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)")
}
}
// 删除 "orders" 域中的所有项目
CSSearchableIndex.default().deleteSearchableItems(
withDomainIdentifiers: ["orders"]
) { error in }
// 核选项:删除所有内容
CSSearchableIndex.default().deleteAllSearchableItems { error in
if let error = error {
print("Failed to delete all: \(error)")
}
}
何时删除:
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())")
}
}
// 从实体创建可搜索项
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
)
// 与 App Intent 实体关联
item.associateAppEntity(orderEntity, priority: .default)
好处:
NSUserActivity 捕获用户参与度,用于:
平台支持 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")
// 启用 Spotlight 搜索
activity.isEligibleForSearch = true
// 启用 Siri 预测
activity.isEligibleForPrediction = true
// 启用到其他设备的接力
activity.isEligibleForHandoff = true
// 将 URL 贡献给全局搜索(仅限公开内容)
activity.isEligibleForPublicIndexing = false
隐私说明 仅对可公开访问的内容(例如,具有公开 URL 的博客文章)设置 isEligibleForPublicIndexing = true。
func viewOrder(_ order: Order) {
// 1. 创建活动
let activity = NSUserActivity(activityType: "com.coffeeapp.viewOrder")
activity.title = order.coffeeName
// 2. 设置资格
activity.isEligibleForSearch = true
activity.isEligibleForPrediction = true
// 3. 提供用于更新/删除的标识符
activity.persistentIdentifier = order.id.uuidString
// 4. 提供丰富的元数据
let attributes = CSSearchableItemAttributeSet(contentType: .item)
attributes.title = order.coffeeName
attributes.contentDescription = "Your \(order.coffeeName) order"
attributes.thumbnailData = order.imageData
activity.contentAttributeSet = attributes
// 5. 标记为当前活动
activity.becomeCurrent()
// 6. 存储引用(重要!)
self.userActivity = activity
}
关键 保持对活动的强引用。没有强引用,它将不会出现在搜索中。
// UIKit 模式
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() // 标记为活动状态
self.currentActivity = activity
self.userActivity = activity // UIKit 集成
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
currentActivity?.resignCurrent() // 标记为非活动状态
}
}
// SwiftUI 模式
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 自动管理 userActivity
self.userActivity = activity
}
}
}
将 NSUserActivity 连接到 App Intent 实体。
func viewOrder(_ order: Order) {
let activity = NSUserActivity(activityType: "com.app.viewOrder")
activity.title = order.coffeeName
activity.isEligibleForSearch = true
activity.isEligibleForPrediction = true
// 连接到 App Intent 实体
activity.appEntityIdentifier = order.id.uuidString
// 现在 Spotlight 可以将此作为实体建议显示
activity.becomeCurrent()
self.userActivity = activity
}
好处:
来自 WWDC 的模式 标记当前可见的内容,以便为 Spotlight 参数提供建议。
func showEvent(_ event: Event) {
let activity = NSUserActivity(activityType: "com.app.viewEvent")
activity.persistentIdentifier = event.id.uuidString
// Spotlight 建议将此事件用于 intent 参数
activity.appEntityIdentifier = event.id.uuidString
activity.becomeCurrent()
userActivity = activity
}
结果 当用户调用需要事件参数的 intent 时,Spotlight 会建议当前可见的事件。
对于快速备忘录链接,活动必须:
becomeCurrent())title(名词,而非动词)let activity = NSUserActivity(activityType: "com.app.viewNote")
activity.title = note.title // ✅ "Project Ideas" 而非 ❌ "View Note"
activity.persistentIdentifier = note.id.uuidString
activity.targetContentIdentifier = note.id.uuidString
activity.becomeCurrent()
当用户点击 Spotlight 结果时,处理继续操作:
// AppDelegate 或 SceneDelegate
func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
guard userActivity.activityType == "com.app.viewOrder" else {
return false
}
// 提取标识符
if let identifier = userActivity.persistentIdentifier,
let orderID = UUID(uuidString: identifier) {
// 导航到订单
navigateToOrder(orderID)
return true
}
return false
}
@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) {
// 导航到订单
navigateToOrder(orderID)
}
}
}
}
}
// 当从 CSSearchableItem 继续时
func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
if userActivity.activityType == CSSearchableItemActionType {
// 从 Core Spotlight 项获取标识符
if let identifier = userActivity.userInfo?[CSSearchableItemActivityIdentifier] as? String {
// 根据标识符导航
navigateToItem(identifier)
return true
}
}
return false
}
NSUserActivity.deleteAllSavedUserActivities { }
let identifiers = ["order-1", "order-2"]
NSUserActivity.deleteSavedUserActivities(
withPersistentIdentifiers: identifiers
) { }
何时删除:
| 方面 | NSUserActivity | CSSearchableItem |
|---|---|---|
| 用途 | 当前用户活动 | 索引所有内容 |
| 何时使用 | 用户查看屏幕时 | 后台内容索引 |
| 范围 | 一次一个项目 | 批量操作 |
| 接力 | 支持 | 不支持 |
| 预测 | 支持 | 不支持 |
| 搜索 | 有限 | 完整的 Spotlight 集成 |
| 示例 | 用户查看订单详情 | 索引所有 500 个订单 |
推荐 两者都使用:
CSSearchableIndex.default().fetchLastClientState { clientState, error in
if let error = error {
print("Error fetching client state: \(error)")
} else {
print("Client state: \(clientState?.base64EncodedString() ?? "none")")
}
}
isEligibleForSearch = trueisEligibleForHandoff = trueapplication(_:continue:restorationHandler:) 是否已实现// 不好:索引所有 10,000 个项目
let allItems = try await ItemService.shared.all()
// 好:索引最近/重要的项目
let recentItems = try await ItemService.shared.recent(limit: 100)
let favoriteItems = try await ItemService.shared.favorites()
原因 性能、配额限制、用户体验。
// 难以删除所有订单
CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: allOrderIDs)
// 轻松删除所有订单
CSSearchableIndex.default().deleteSearchableItems(withDomainIdentifiers: ["orders"])
// 不好:项目永不过期
let item = CSSearchableItem(/* ... */)
// 好:1 年后过期
item.expirationDate = Date().addingTimeInterval(60 * 60 * 24 * 365)
attributes.title = "Item"
attributes.title = "Medium Latte Order"
attributes.contentDescription = "Ordered on December 12, 2025"
attributes.keywords = ["coffee", "latte", "order", "medium"]
attributes.thumbnailData = imageData
func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
guard let identifier = userActivity.persistentIdentifier else {
return false
}
// 尝试加载内容
if let item = try? await ItemService.shared.fetch(id: identifier) {
navigate(to: item)
return true
} else {
// 内容已删除或不可用
showAlert("This content is no longer available")
// 从搜索中删除活动
NSUserActivity.deleteSavedUserActivities(
withPersistentIdentifiers: [identifier]
)
return true // 仍然已处理
}
}
import CoreSpotlight
import UniformTypeIdentifiers
class OrderManager {
// MARK: - Core Spotlight 索引
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) {
// 从 Core Spotlight 删除
CSSearchableIndex.default().deleteSearchableItems(
withIdentifiers: [orderID.uuidString]
)
// 删除 NSUserActivity
NSUserActivity.deleteSavedUserActivities(
withPersistentIdentifiers: [orderID.uuidString]
)
}
func deleteAllOrders() {
CSSearchableIndex.default().deleteSearchableItems(
withDomainIdentifiers: ["orders"]
)
}
// MARK: - 用于当前屏幕的 NSUserActivity
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
// 连接到 App Intents
activity.appEntityIdentifier = order.id.uuidString
// 丰富的元数据
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 视图控制器
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 视图
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,以实现全面的可发现性。
每周安装次数
87
代码仓库
GitHub 星标数
601
首次出现时间
Jan 21, 2026
安全审计
安装于
opencode72
claude-code68
codex67
gemini-cli65
cursor65
github-copilot62
Comprehensive guide to Core Spotlight framework and NSUserActivity for making app content discoverable in Spotlight search, enabling Siri predictions, and supporting Handoff. Core Spotlight directly indexes app content while NSUserActivity captures user engagement for prediction.
Key distinction Core Spotlight = indexing all app content; NSUserActivity = marking current user activity for prediction/handoff.
Use this skill when:
Do NOT use this skill for:
| Use Case | Approach | Example |
|---|---|---|
| User viewing specific screen | NSUserActivity | User opened order details |
| Index all app content | CSSearchableItem | All 500 orders searchable |
| App Intents entity search | IndexedEntity | "Find orders where..." |
| Handoff between devices | NSUserActivity | Continue editing note on Mac |
| Background content indexing | CSSearchableItem batch |
Apple guidance Use NSUserActivity for user-initiated activities (screens currently visible), not as a general indexing mechanism. For comprehensive content indexing, use Core Spotlight's 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)")
}
}
}
Purpose Stable, persistent ID unique to this item within your app.
uniqueIdentifier: order.id.uuidString
Requirements:
Purpose Groups related items for bulk operations.
domainIdentifier: "orders"
Use cases:
Pattern:
// Index with domains
item1.domainIdentifier = "orders"
item2.domainIdentifier = "documents"
// Delete entire domain
CSSearchableIndex.default().deleteSearchableItems(
withDomainIdentifiers: ["orders"]
) { error in }
Metadata describing the searchable content.
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"
| Attribute | Purpose | Example |
|---|---|---|
title | Primary title | "Coffee Order #1234" |
displayName | User-visible name | "Morning Latte" |
contentDescription | Description text | "Medium latte with oat milk" |
keywords | Search terms | ["coffee", "latte"] |
thumbnailData | Preview image |
// 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 = ["jane@example.com"]
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")
}
}
Recommended batch size 100-500 items per call. For larger sets, split into multiple batches.
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)")
}
}
When to delete:
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)
Benefits:
NSUserActivity captures user engagement for:
Platform support 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
Privacy note Only set isEligibleForPublicIndexing = true for publicly accessible content (e.g., blog posts with public URLs).
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
}
Critical Maintain strong reference to activity. It won't appear in search without one.
// 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
}
}
}
Connect NSUserActivity to App Intent entities.
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
}
Benefits:
Pattern from WWDC Tag currently visible content for Spotlight parameter suggestions.
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
}
Result When users invoke intents requiring an event parameter, Spotlight suggests the currently visible event.
For Quick Note linking, activities must:
becomeCurrent())title (nouns, not verbs)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()
When users tap Spotlight results, handle continuation:
// 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
}
@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
}
NSUserActivity.deleteAllSavedUserActivities { }
let identifiers = ["order-1", "order-2"]
NSUserActivity.deleteSavedUserActivities(
withPersistentIdentifiers: identifiers
) { }
When to delete:
| Aspect | NSUserActivity | CSSearchableItem |
|---|---|---|
| Purpose | Current user activity | Indexing all content |
| When to use | User viewing a screen | Background content indexing |
| Scope | One item at a time | Batch operations |
| Handoff | Supported | Not supported |
| Prediction | Supported | Not supported |
| Search | Limited | Full Spotlight integration |
| Example | User viewing order detail | Index all 500 orders |
Recommended Use both:
CSSearchableIndex.default().fetchLastClientState { clientState, error in
if let error = error {
print("Error fetching client state: \(error)")
} else {
print("Client state: \(clientState?.base64EncodedString() ?? "none")")
}
}
isEligibleForSearch = trueisEligibleForHandoff = trueapplication(_:continue:restorationHandler:) implemented// 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()
Why Performance, quota limits, user experience.
// Hard to delete all orders
CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: allOrderIDs)
// Easy to delete all orders
CSSearchableIndex.default().deleteSearchableItems(withDomainIdentifiers: ["orders"])
// Bad: Items never expire
let item = CSSearchableItem(/* ... */)
// Good: Expire after 1 year
item.expirationDate = Date().addingTimeInterval(60 * 60 * 24 * 365)
attributes.title = "Item"
attributes.title = "Medium Latte Order"
attributes.contentDescription = "Ordered on December 12, 2025"
attributes.keywords = ["coffee", "latte", "order", "medium"]
attributes.thumbnailData = imageData
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
Docs : /corespotlight, /corespotlight/cssearchableitem, /foundation/nsuseractivity
Skills : axiom-app-intents-ref, axiom-app-discoverability, axiom-app-shortcuts-ref
Remember Core Spotlight indexes all your app's content; NSUserActivity marks what the user is currently doing. Use CSSearchableItem for batch indexing, NSUserActivity for active screens, and connect them to App Intents with appEntityIdentifier for comprehensive discoverability.
Weekly Installs
87
Repository
GitHub Stars
601
First Seen
Jan 21, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode72
claude-code68
codex67
gemini-cli65
cursor65
github-copilot62
药物不良事件信号检测工具 - FAERS数据分析、安全信号评分与药物警戒
137 周安装
飞书任务管理工具:自动化创建、更新、评论、附件及任务清单管理
142 周安装
Paseo任务交接技能:AI智能体无缝任务转移与上下文传递指南
204 周安装
Elastic APM 服务健康评估指南:使用 Observability APIs 与 ES|QL 监控 SLO、延迟、错误率
199 周安装
Encore Go 应用测试指南:使用 encore test 命令进行 API、数据库和服务测试
155 周安装
Kubernetes安全策略指南:NetworkPolicy、PodSecurityPolicy、RBAC与Pod安全标准
99 周安装
| Index documents on launch |
| JPEG/PNG data |
contentCreationDate | When created | Date() |
contentModificationDate | Last modified | Date() |
rating | Star rating | NSNumber(value: 5) |
latitude / longitude | Location | 37.7749, -122.4194 |