WWDC Quick Look 💓 By SwiftGGTeam
What's new in App Intents

What's new in App Intents

观看原视频

Highlight

App Intents 在 2024 年引入了四个关键更新:IndexedEntity 让你的实体能被 Spotlight 索引,Transferable 让实体可转换为标准格式,URLRepresentable 让 Intent 类型映射为 Universal Links,以及 UnionValue 等开发者体验改进。


核心内容

过去,你的 App 内容只能以 CSSearchableItem 的形式出现在 Spotlight 中。这限制了搜索能力——用户找到内容后,系统无法进一步操作。而且 Siri 对你 App 内部的实体理解有限,只能基于关键词匹配,无法真正理解内容的语义。

2024 年,Apple 推出了 IndexedEntity 协议。你的 App Entity 现在可以直接捐赠给 Spotlight,不需要再通过 CSSearchableItem 中转。更重要的是,这打通了 Siri 和 Apple Intelligence 理解你 App 内容的通道。当你在 Spotlight 中搜索一条步道时,Siri 不只是匹配文字,而是理解这是一个”Trail”实体,知道它有什么属性、可以执行什么操作。

另一个痛点是跨 App 分享。用户想把你的 App 内容分享到邮件或社交媒体时,过去只能通过剪贴板或自定义分享扩展。现在,通过让 Entity 遵循 Transferable 协议,你的内容可以直接转换为 PDF、图片、富文本等标准格式,被任何接受这些格式的 App 接收。

Universal Links 的集成则解决了深度链接问题。过去,Intent 类型无法直接映射为 URL,导致 Shortcuts 和 Siri 难以跳转到 App 的特定页面。现在,URLRepresentableIntent 让 Intent 类型自动生成 Universal Link,复用你现有的 URL 处理代码。


详细内容

IndexedEntity:让实体被 Spotlight 索引

IndexedEntity 是一个新的协议,让你的 App Entity 可以直接被 CSSearchableIndex 索引(02:02)。最基础的实现只需要遵循协议,然后在 App 启动时调用 indexAppEntities

// TrailEntity 遵循 IndexedEntity 协议
struct TrailEntity: IndexedEntity {
    // 默认实现使用 DisplayRepresentation 填充属性集
}

// 在 App init 方法中索引所有实体
class AppDelegate {
    init() {
        let trails = DataManager.shared.trails
        CSSearchableIndex.default().indexAppEntities(trails)
    }
}

关键点:

  • IndexedEntity 协议自动将 DisplayRepresentation 转换为 CSSearchableItem 的属性集
  • indexAppEntities 方法接受 AppEntity 数组,一次性索引多个实体
  • 索引后,实体会在 Spotlight 搜索结果中显示,并可以关联到你的 Intent

默认实现只使用 DisplayRepresentation。你可以进一步自定义 attributeSet,提供位置信息、关键词等额外数据(02:47):

extension TrailEntity {
    var attributeSet: CSSearchableItemAttributeSet {
        let attributeSet = CSSearchableItemAttributeSet(contentType: .UTTypeText)
        // 添加位置信息
        attributeSet.city = location.city
        attributeSet.state = location.state
        // 添加关键词帮助搜索
        attributeSet.keywords = activities.map { $0.rawValue }
        return attributeSet
    }
}

关键点:

  • attributeSet 计算属性允许你自定义索引的元数据
  • 可以添加位置信息(citystate)提升本地搜索相关性
  • keywords 字段添加同义词或相关词汇,帮助用户找到内容

如果你已经在用 CSSearchableItem 索引内容,可以用新的 associateAppEntity 方法把现有索引和 App Entity 关联起来(04:12),无需重新实现索引逻辑。

Transferable:让实体可跨 App 分享

Transferable 是一个声明性的协议,描述你的模型如何序列化和反序列化。现在,App Entity 也可以遵循 Transferable(05:05):

// Activity Summary Entity 遵循 Transferable 协议
struct ActivitySummaryEntity: AppEntity, Transferable {
    static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "活动摘要")
    static var defaultQuery = ActivitySummaryQuery()

    var id: String
    var date: Date
    var caloriesBurned: Int
    var distance: Double

    // 定义可传输的表示形式
    static var transferRepresentation: some TransferRepresentation {
        // 1. 最高保真度:私有 Codable 表示
        DataRepresentation(contentType: .applicationJSON) { summary in
            try JSONEncoder().encode(summary)
        } importing: { data in
            try JSONDecoder().decode(ActivitySummaryEntity.self, from: data)
        }

        // 2. 富文本格式
        DataRepresentation(contentType: .rtf) { summary in
            summary.toRichText()
        }

        // 3. 图片格式
        FileRepresentation(contentType: .png) { summary in
            let image = summary.generateChartImage()
            return SavedFileRepresentable(image)
        }

        // 4. 纯文本格式(最低保真度)
        DataRepresentation(contentType: .plainText) { summary in
            summary.description.data(using: .utf8)!
        }
    }
}

关键点:

  • transferRepresentation 是静态计算属性,返回支持的表示形式列表
  • 表示形式的顺序很重要:从最高保真度到最低保真度排列
  • DataRepresentation 用于内存中的数据(如 JSON、RTF、文本)
  • FileRepresentation 用于文件类型(如 PNG、PDF)
  • SavedFileRepresentable 包装生成的临时文件,供其他 App 访问

在 Shortcuts 中,当你的 Entity 被传递给接受不同类型参数的 Action 时,App Intents 会自动选择最合适的表示形式进行转换(07:02)。

目前 Transferable 与 AppEntity 结合使用有一些限制(08:00):Xcode 需要在编译时理解你的 Transferable 表示,ProxyRepresentation 只能引用带 @Property 属性的实体属性。

IntentFile:接收可转换内容

当你的 Intent 接受 IntentFile 参数时,可以声明支持的内容类型(08:49):

struct AppendToNoteIntent: AppIntent {
    static var description = "将内容追加到备忘录"

    @Parameter(title: "附件")
    var attachment: IntentFile

    @Parameter(title: "备忘录")
    var note: NoteEntity

    static var parameterSummary = some ParameterSummary {
        Summary("追加 \(\.$attachment)\(\.$note)")
    }

    func perform() async throws -> some IntentResult & ReturnsValue<NoteEntity> {
        // 检查文件类型
        if attachment.contentType == .plainText {
            let textContent = try await attachment.textContent
            note.append(text: textContent)
        } else if attachment.contentType == .png {
            let imageURL = try await attachment.fileURL
            note.append(imageFrom: imageURL)
        }

        return .result(value: note)
    }
}

关键点:

  • IntentFile 参数可以接受任何遵循 Transferable 的 Entity
  • contentType 属性检查实际的内容类型
  • textContentfileURL 是新的 API,用于提取内容
  • Siri 和 Shortcuts 会自动将 Entity 转换为 IntentFile 支持的类型

FileEntity:文件即实体

对于基于文档的 App,或管理文件的应用,可以用 FileEntity 让文件本身成为 Entity(10:18):

struct PhotoEntity: AppEntity, FileEntity {
    static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "照片")
    static var defaultQuery = PhotoQuery()

    var id: FileEntityIdentifier
    var name: String

    // FileEntity 要求的属性
    static var supportedContentTypes: [UTType] {
        [.image, .png, .jpeg]
    }

    // 使用文件 URL 作为标识符
    init(url: URL) {
        self.id = FileEntityIdentifier(url: url)
        self.name = url.lastPathComponent
    }
}

关键点:

  • FileEntity 是特殊的 AppEntity,表示文件本身
  • supportedContentTypes 声明支持的文件类型
  • FileEntityIdentifier 使用 URL 的书签数据,文件移动或重命名后仍然有效
  • 其他 App 可以通过 IntentFile 安全地访问这些文件

URLRepresentable:深度链接

URLRepresentableEntityURLRepresentableIntent 让你的 Entity 和 Intent 可以表示为 Universal Link(12:06):

// Entity 支持链接表示
extension TrailEntity: URLRepresentableEntity {
    static var urlRepresentation: URLRepresentation {
        URLRepresentation(string: "https://trails.example.com/trail/\(Self.self.\.id)")
    }
}

// Intent 支持链接表示
struct OpenTrailIntent: AppIntent, OpenIntent, URLRepresentableIntent {
    static var description = "打开步道详情"
    static var openUrlWhenRun: Bool = true

    @Parameter(title: "步道")
    var trail: TrailEntity

    static var parameterSummary = some ParameterSummary {
        Summary("打开 \(\.$trail)")
    }

    // 不需要实现 perform,App Intents 会调用 URL 处理代码
}

关键点:

  • URLRepresentation 使用字符串插值,可以插入带 @Property 的属性
  • URLRepresentableIntentOpenIntent 结合,自动处理 URL 打开
  • openUrlWhenRun 控制是否在执行时打开 URL
  • 不需要实现 perform,框架会复用你现有的 Universal Link 处理代码

另一种方式是在 perform 中返回 OpenURLIntent14:24):

struct CreateTrailIntent: AppIntent {
    func perform() async throws -> some IntentResult & OpensIntent {
        // 创建新步道
        let newTrail = try await createTrail()

        // 返回 OpenURLIntent 打开新创建的步道
        return .result(opensIntent: OpenURLIntent(newTrail))
    }
}

UnionValue:联合类型参数

新加入的 UnionValue 宏允许参数接受多种类型之一(14:38):

enum DayPassType: UnionValue {
    case park(ParkEntity)
    case trail(TrailEntity)
}

struct BuyDayPassIntent: AppIntent {
    static var description = "购买日票"

    @Parameter(title: "票种")
    var passType: DayPassType

    func perform() async throws -> some IntentResult {
        switch passType {
        case .park(let park):
            try await purchaseParkPass(for: park)
        case .trail(let trail):
            try await purchaseTrailPass(for: trail)
        }

        return .result()
    }
}

关键点:

  • UnionValue 宏标记枚举为联合值类型
  • 每个 case 必须恰好有一个关联值
  • 所有关联值的类型必须互不相同
  • 可以理解为”或”参数:ParkEntity 或 TrailEntity

开发者体验改进

Xcode 16.0 不再强制要求为 AppEntity 属性或 AppIntent 参数提供 title(16:02):

// Xcode 16 之前:必须提供 title
@Parameter(title: "步道集合")
var trailCollection: TrailCollection

// Xcode 16:title 可选
@Parameter
var trailCollection: TrailCollection  // 自动生成 "Trail Collection" 作为 title

Xcode 会基于属性名智能生成 title。如果你需要自定义标题(如”Featured Collection”而非”TrailCollection”),仍然可以手动指定。

Framework 支持也有所改进(17:05):现在可以在 Framework 中定义 AppEntity,然后在 App 或 Extension 中引用它。过去所有类型必须在同一模块内。


核心启发

  1. 立即为核心内容添加 IndexedEntity

    为什么值得做:这是让 Apple Intelligence 理解你 App 内容的最直接途径。几行代码就能让你的实体出现在 Spotlight 和 Siri 建议中,用户无需打开 App 就能发现和操作你的内容。

    怎么开始:从你最常用的 Entity 开始(如文档、项目、文章),添加 IndexedEntity 遵循,在 App 启动时调用 indexAppEntities。如果你已有 CSSearchableItem 代码,用 associateAppEntity 连接两者。

  2. 为可分享内容实现 Transferable

    为什么值得做:用户经常需要把你的 App 内容分享到邮件、笔记或社交媒体。Transferable 让你的内容能以标准格式(PDF、图片、富文本)被任何 App 接收,大幅提升分享体验。

    怎么开始:识别你 App 中用户可能想分享的内容(如报告、摘要、图表),为对应的 Entity 添加 Transferable 支持。按保真度排序表示形式:先是你的私有格式(用于同 App 传输),然后是通用格式(RTF、PNG、PDF),最后是纯文本。

  3. 用 URLRepresentable 简化深度链接

    为什么值得做:Universal Links 是深度链接的标准方式。通过 URLRepresentableIntent,你的 Intent 自动生成正确的 URL,复用现有的路由代码,无需重复实现导航逻辑。

    怎么开始:为需要跳转的 Entity 添加 URLRepresentableEntity,为打开类 Intent 添加 URLRepresentableIntent。确保你的 URL 模板包含 Entity 的 ID 或其他唯一标识符。

  4. 用 UnionValue 简化参数类型

    为什么值得做:当用户需要在不同类型的对象上执行相同操作时(如”购买日票”可以是公园票或步道票),UnionValue 提供了一种类型安全的方式表达”或”关系,比创建两个单独的 Intent 更清晰。

    怎么开始:寻找接受多种类似类型参数的 Intent,将它们合并为使用 UnionValue 的单个 Intent。确保每个 case 的关联值类型互不相同。


关联 Session

评论

GitHub Issues · utterances