mcpField
A
axiom-core-spotlight-ref

axiom-core-spotlight-ref

github.com/charleswiltgen开发1.2k163 次安装今天更新
代码仓库

关于 axiom-core-spotlight-ref

为 Apple 应用开发者提供 Core Spotlight 和 NSUserActivity 参考,区分内容索引与当前活动记录,并介绍 Spotlight 搜索、Siri 预测、Handoff 和 App Intents。

主要功能

  1. 01使用 CSSearchableItem 为内容建索引
  2. 02使用 NSUserActivity 记录活动
  3. 03区分内容索引与活动 API
  4. 04批量索引并删除已索引项目
  5. 05将活动关联到 App Intents

适用场景

  1. 01让应用文档或消息可在 Spotlight 中搜索
  2. 02支持从当前查看的界面使用 Handoff
  3. 03为应用活动启用 Siri 预测
  4. 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“查找符合条件的订单……”
在设备之间使用 HandoffNSUserActivity在 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 必须:

  1. 是应用的当前 activity(通过 becomeCurrent())
  2. 具有清晰、简洁的 title(使用名词,而非动词)
  3. 提供稳定且一致的标识符
  4. 始终支持导航到关联内容
  5. 妥善处理内容缺失的情况
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 对比

方面NSUserActivityCSSearchableItem
用途当前用户活动为所有内容编制索引
使用时机用户正在查看某个屏幕在后台为内容编制索引
范围一次处理一个项目批量操作
Handoff支持不支持
预测支持不支持
搜索有限完整集成 Spotlight
示例用户正在查看订单详情为全部 500 个订单编制索引

建议 同时使用:

  • 对当前可见的屏幕使用 NSUserActivity
  • 使用 CSSearchableItem 为内容建立全面索引

测试与调试

验证已编制索引的项目
使用 Spotlight
  1. 打开 Spotlight(在主屏幕上向下轻扫)
  2. 搜索已编制索引内容中的关键词
  3. 确认应用结果是否显示
  4. 点按结果 → 确认导航是否正常
使用控制台日志
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 关联,以实现全面的可发现性。

评论

支持 Markdown