AppEntity
是 App Intents 框架中的一个协议,用来让系统(如 Siri、快捷指令、Spotlight 等)识别你 App 中的某一类对象,比如:
任务(Task)
待办事项(ToDo)
计时器(Timer)
音乐、相册、联系人……
你 App 中任何可操作的“实体”
换句话说,它是让你的 App 中的「对象」能成为 Siri / 快捷指令的输入和输出单位的桥梁。
🧱 AppEntity 的结构 #
struct Task: AppEntity {
var id: String
var title: String
static var typeDisplayRepresentation: TypeDisplayRepresentation {
"Task"
}
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: title)
}
static var defaultQuery = TaskQuery()
}
必须实现的核心部分:
部分 | 说明 |
---|---|
id | 实体的唯一标识 |
typeDisplayRepresentation | Siri 显示这个类型用什么名字(比如“任务”、“计时器”) |
displayRepresentation | 显示具体某个实例时的名称(比如“写日报”) |
defaultQuery | 提供搜索、选择功能的数据源入口(一般是一个查询器) |
🧪 示例:你的计时器项目中加入 AppEntity #
Step 1:定义 Timer 实体 #
import AppIntents
struct FocusTimer: AppEntity, Identifiable {
var id: String
var name: String
var duration: Int
static var typeDisplayRepresentation: TypeDisplayRepresentation {
"Focus Timer"
}
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: "\(name) (\(duration) min)")
}
static var defaultQuery = FocusTimerQuery()
}
Step 2:定义实体查询器 #
你必须提供一个 EntityQuery
来告诉系统有哪些可选对象可以让用户挑选。
struct FocusTimerQuery: EntityQuery {
func entities(for identifiers: [FocusTimer.ID]) async throws -> [FocusTimer] {
// 通过 id 查找实体(通常来自你的数据库)
sampleTimers.filter { identifiers.contains($0.id) }
}
func suggestedEntities() async throws -> [FocusTimer] {
// 默认展示的候选项
sampleTimers
}
var sampleTimers: [FocusTimer] {
[
FocusTimer(id: "1", name: "写作", duration: 25),
FocusTimer(id: "2", name: "读书", duration: 15),
FocusTimer(id: "3", name: "思考", duration: 30)
]
}
}
Step 3:让 Intent 使用这个实体 #
struct StartNamedTimerIntent: AppIntent {
static var title: LocalizedStringResource = "Start Named Timer"
@Parameter(title: "Focus Session")
var timer: FocusTimer
func perform() async throws -> some IntentResult {
TimerController.shared.startTimer(for: timer.duration)
return .result(dialog: "Started \(timer.name) for \(timer.duration) minutes.")
}
}
系统会自动提供一个“选择计时器”的 UI,让用户选择
写作
/读书
等任务。
✅ 你为什么需要用 AppEntity? #
你希望支持的功能 | 是否需要 AppEntity? |
---|---|
Siri 语音选择任务对象 | ✅ |
快捷指令中提供列表选择 | ✅ |
在 Widget 中显示任务列表 | ✅ |
AppIntent 中传入自定义数据类型 | ✅ |
简单数字 / 字符串参数 | ❌ |
🧩 总结 #
名称 | 用途 |
---|---|
AppIntent | 一个操作(如“开始计时器”) |
@Parameter | AppIntent 的输入参数 |
AppEntity | App 中的数据对象(如任务、计时器、事项) |
EntityQuery | 提供 AppEntity 的搜索、列表数据 |
AppShortcutsProvider | 系统级快捷方式展示支持 |
想让我帮你在当前的 Live Activity 项目中加入 AppEntity
版本的计时器选择吗?可以让用户对 Siri 说 “开始写作计时器”,然后自动开始 Live Activity。
很好!下面我将帮助你在当前的计时器 + Live Activity 项目中,加入基于 AppEntity
的计时器选择功能。最终你可以实现:
用户对 Siri 说:“开始写作计时器”,然后 App 通过
AppEntity
识别 “写作”,并自动启动对应的 Live Activity。
✅ 功能目标回顾 #
定义多个预设计时器:写作 / 阅读 / 思考等
使用
FocusTimer
作为 AppEntity用户在快捷指令 / Siri / Widget 中可以选择这些计时器
启动后在灵动岛中展示对应名称和时间
🧱 项目结构更新 #
我们将新增三个文件:
StartNamedTimerIntent.swift ← Intent,带实体参数
FocusTimer.swift ← AppEntity
FocusTimerQuery.swift ← 查询器(数据源)
🧩 步骤一:定义 AppEntity(FocusTimer.swift) #
import AppIntents
struct FocusTimer: AppEntity, Identifiable {
var id: String
var name: String
var duration: Int
static var typeDisplayRepresentation: TypeDisplayRepresentation {
"Focus Timer"
}
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: "\(name) (\(duration) 分钟)")
}
static var defaultQuery = FocusTimerQuery()
}
🧩 步骤二:定义实体查询器(FocusTimerQuery.swift) #
import AppIntents
struct FocusTimerQuery: EntityQuery {
func entities(for identifiers: [FocusTimer.ID]) async throws -> [FocusTimer] {
allTimers.filter { identifiers.contains($0.id) }
}
func suggestedEntities() async throws -> [FocusTimer] {
allTimers
}
var allTimers: [FocusTimer] {
[
FocusTimer(id: "1", name: "写作", duration: 25),
FocusTimer(id: "2", name: "阅读", duration: 15),
FocusTimer(id: "3", name: "冥想", duration: 10)
]
}
}
🧩 步骤三:创建 AppIntent(StartNamedTimerIntent.swift) #
import AppIntents
import ActivityKit
struct StartNamedTimerIntent: AppIntent {
static var title: LocalizedStringResource = "Start Named Focus Timer"
static var description = IntentDescription("Start a specific focus timer by name and show it in the Dynamic Island.")
@Parameter(title: "Which Focus Timer")
var timer: FocusTimer
func perform() async throws -> some IntentResult {
let duration = TimeInterval(timer.duration * 60)
let attributes = TimerAttributes(taskName: timer.name)
let contentState = TimerAttributes.ContentState(remainingTime: duration)
do {
_ = try Activity<TimerAttributes>.request(attributes: attributes, contentState: contentState)
return .result(dialog: "已启动「\(timer.name)」计时器,共 \(timer.duration) 分钟")
} catch {
return .result(dialog: "启动失败,请稍后重试")
}
}
}
🧭 Siri / 快捷方式体验效果 #
打开快捷指令 App
添加操作 → 搜索你的 App → “Start Named Focus Timer”
你将看到一个计时器选择列表(写作、阅读、冥想)
运行后启动对应 Live Activity,并显示在灵动岛
Siri 语音:说“开始写作计时器”也会识别到!
✅ 总结 #
模块 | 用途 |
---|---|
FocusTimer | 定义一个具体的计时器(AppEntity) |
FocusTimerQuery | 系统调用时如何获取可选对象 |
StartNamedTimerIntent | 使用实体启动计时器并触发 Live Activity |
是否需要我帮你加上:
支持“结束计时器”的 AppIntent?
使用
@IntentDialog
提供更自然的 Siri 对话语句?动态 Live Activity 自动更新倒计时进度?
这些都可以继续扩展这个项目。告诉我你的优先级即可。
非常好!我们将继续扩展你的 计时器 + AppEntity + Live Activity 项目,加入以下高级功能:
🎯 目标 #
✅ 当前已有:支持用 AppEntity 启动指定计时器(写作 / 阅读 / 冥想…)
🆕 本次新增:
✅ 支持自动更新 Live Activity 倒计时(自动递减)
✅ 添加一个 “结束计时器” 的 AppIntent
✅ 使用
@IntentDialog
让 Siri 对话更自然
🧩 一、Live Activity 倒计时自动更新 #
我们需要持续更新 Activity 的 remainingTime
,你可以使用定时器或后台任务管理。
修改 TimerAttributes.ContentState
#
你已有:
struct ContentState: Codable, Hashable {
var remainingTime: TimeInterval
}
添加一个后台控制器类:TimerController.swift
#
import ActivityKit
class TimerController: ObservableObject {
static let shared = TimerController()
private var activity: Activity<TimerAttributes>?
private var timer: Timer?
func startActivity(taskName: String, duration: TimeInterval) {
let attr = TimerAttributes(taskName: taskName)
let state = TimerAttributes.ContentState(remainingTime: duration)
do {
activity = try Activity<TimerAttributes>.request(attributes: attr, contentState: state)
startCountdown(duration: duration)
} catch {
print("启动 Live Activity 失败:\(error)")
}
}
func startCountdown(duration: TimeInterval) {
var remaining = duration
timer?.invalidate()
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
remaining -= 1
Task {
await self.activity?.update(using: .init(remainingTime: remaining))
}
if remaining <= 0 {
self.endActivity()
}
}
}
func endActivity() {
timer?.invalidate()
Task {
await activity?.end(using: .init(remainingTime: 0), dismissalPolicy: .immediate)
}
}
}
🧩 二、添加结束计时器的 AppIntent #
新建文件:StopTimerIntent.swift
import AppIntents
struct StopTimerIntent: AppIntent {
static var title: LocalizedStringResource = "Stop Timer"
static var description = IntentDescription("Ends the current focus timer and removes the Live Activity.")
@IntentDialog("你想停止正在进行的计时器吗?")
static var dialog: IntentDialog
func perform() async throws -> some IntentResult {
TimerController.shared.endActivity()
return .result(dialog: "已结束计时器")
}
}
🧩 三、升级 AppIntent 的 Siri 语义(对话自然化) #
更新 StartNamedTimerIntent.swift
:
@IntentDialog("开始「\(.parameter(\.timer))」计时器,对吗?")
static var dialog: IntentDialog
让 Siri 对话效果更自然,例如:
Siri: “开始「写作(25分钟)」计时器,对吗?”
你:“是的”
Siri: “已启动”
✅ 项目更新清单 #
文件 | 说明 |
---|---|
TimerController.swift | 控制 Live Activity 自动更新 |
StopTimerIntent.swift | 新增:支持结束计时器 |
StartNamedTimerIntent.swift | 更新:加上 Siri 对话优化 |
Live Activity Widget | (已完成) |
🔚 总结体验 #
你现在支持:
✅ 用 Siri/快捷指令 选择计时器并启动
✅ 自动显示灵动岛 Live Activity
✅ 实时倒计时进度更新
✅ 语音对话自然(AppIntent + IntentDialog)
✅ 支持结束当前计时器
是否需要我:
加上任务记录(把每个完成的计时器存入 Core Data / UserDefaults)?
做一个交互式 Widget,能点一下就启动一个指定计时器?
加入多语言支持(中英文 AppIntent title / description)?
告诉我你想继续哪个方向,我可以继续帮你构建。
很好!我们现在将为你的计时器 + Live Activity 项目继续添加以下功能:
✅ 新增目标 #
任务记录:每次完成的计时器都自动记录(使用 UserDefaults 实现基础版)
交互式 Widget:支持从主屏幕 Widget 中点选某个计时器,直接启动 Live Activity
🧩 一、任务记录功能(UserDefaults 简易版) #
我们将新增一个记录完成任务的小结构和存储逻辑。
1. 定义模型 CompletedTimer.swift
#
struct CompletedTimer: Identifiable, Codable {
let id = UUID()
let name: String
let duration: Int
let completedAt: Date
}
2. 创建一个管理器 TimerHistoryStore.swift
#
class TimerHistoryStore: ObservableObject {
static let shared = TimerHistoryStore()
@Published private(set) var history: [CompletedTimer] = []
private let key = "completedTimers"
init() {
load()
}
func addRecord(name: String, duration: Int) {
let record = CompletedTimer(name: name, duration: duration, completedAt: .now)
history.insert(record, at: 0)
save()
}
private func load() {
if let data = UserDefaults.standard.data(forKey: key),
let decoded = try? JSONDecoder().decode([CompletedTimer].self, from: data) {
self.history = decoded
}
}
private func save() {
if let data = try? JSONEncoder().encode(history) {
UserDefaults.standard.set(data, forKey: key)
}
}
}
3. 在 TimerController
中结束时记录
#
func endActivity() {
timer?.invalidate()
Task {
await activity?.end(using: .init(remainingTime: 0), dismissalPolicy: .immediate)
if let attributes = activity?.attributes {
TimerHistoryStore.shared.addRecord(name: attributes.taskName, duration: Int(attributes.durationMinutes))
}
}
}
你可以在 SwiftUI 页面中直接使用 @ObservedObject var store = TimerHistoryStore.shared
来展示记录。
🧩 二、交互式 Widget:启动指定计时器 #
WidgetKit + AppIntents 支持 点击 Widget → 执行 Intent。
1. 定义 Widget 配置:TimerLauncherIntent.swift
#
import AppIntents
struct TimerLauncherIntent: AppIntent {
static var title: LocalizedStringResource = "Start Timer from Widget"
@Parameter(title: "Select a Timer")
var timer: FocusTimer
func perform() async throws -> some IntentResult {
TimerController.shared.startActivity(taskName: timer.name, duration: TimeInterval(timer.duration * 60))
return .result()
}
}
2. 创建 Widget #
struct TimerLauncherWidget: Widget {
var body: some WidgetConfiguration {
AppIntentConfiguration(kind: "widget.timer.launcher", intent: TimerLauncherIntent.self, provider: Provider()) { entry in
Text(entry.timer.name)
}
.configurationDisplayName("快速启动计时器")
.description("从主屏幕快速启动写作、阅读等计时器。")
.supportedFamilies([.systemSmall])
}
}
你可以让这个 Widget 通过 FocusTimerQuery
提供写作 / 阅读 / 冥想等选项,点击即启动。
✅ 小结 #
功能 | 技术实现 | 状态 |
---|---|---|
🧾 记录完成的计时任务 | UserDefaults + Codable + ObservableObject | ✅ 已完成 |
⏱ 从 Widget 启动指定计时器 | AppIntentConfiguration + FocusTimer | ✅ 已完成 |
是否需要我帮你:
将任务记录页面做成 SwiftUI 列表(展示过去完成记录)?
将记录同步到 iCloud?
让 Widget 动态展示下一个推荐任务?
告诉我你下一步的方向,我继续为你构建。
很好!我们现在继续为你的计时器项目添加以下功能:
✅ 新增功能目标 #
任务记录页面(SwiftUI 列表):展示用户完成的所有计时任务
iCloud 同步任务记录:通过
@AppStorage
+NSUbiquitousKeyValueStore
同步记录(基础实现)
🧩 一、任务记录页面(SwiftUI 列表) #
我们使用 TimerHistoryStore.shared
来显示记录内容。
TimerHistoryView.swift
#
import SwiftUI
struct TimerHistoryView: View {
@ObservedObject var store = TimerHistoryStore.shared
var body: some View {
NavigationView {
List(store.history) { record in
VStack(alignment: .leading, spacing: 4) {
Text(record.name)
.font(.headline)
Text("时长:\(record.duration) 分钟")
.font(.subheadline)
Text(record.completedAt.formatted(date: .abbreviated, time: .shortened))
.font(.caption)
.foregroundColor(.gray)
}
.padding(.vertical, 4)
}
.navigationTitle("任务记录")
}
}
}
你可以将这个页面作为 TabView 或设置入口显示:
TimerHistoryView()
.tabItem {
Label("记录", systemImage: "list.bullet.rectangle")
}
🧩 二、记录 iCloud 同步功能(基础实现) #
修改 TimerHistoryStore
实现
#
我们将从 UserDefaults
切换为 NSUbiquitousKeyValueStore
,以启用 iCloud 同步。
替换保存与读取逻辑 #
import Foundation
import Combine
class TimerHistoryStore: ObservableObject {
static let shared = TimerHistoryStore()
private let key = "completedTimers"
private let store = NSUbiquitousKeyValueStore.default
@Published private(set) var history: [CompletedTimer] = []
init() {
load()
NotificationCenter.default.addObserver(
self,
selector: #selector(iCloudDidChange(_:)),
name: NSUbiquitousKeyValueStore.didChangeExternallyNotification,
object: store
)
store.synchronize()
}
func addRecord(name: String, duration: Int) {
let record = CompletedTimer(name: name, duration: duration, completedAt: .now)
history.insert(record, at: 0)
save()
}
@objc private func iCloudDidChange(_ notification: Notification) {
load()
}
private func load() {
if let data = store.data(forKey: key),
let decoded = try? JSONDecoder().decode([CompletedTimer].self, from: data) {
DispatchQueue.main.async {
self.history = decoded
}
}
}
private func save() {
if let data = try? JSONEncoder().encode(history) {
store.set(data, forKey: key)
store.synchronize()
}
}
}
开启 iCloud 权限 #
确保在项目中:
打开 Xcode → 项目设置 → “Signing & Capabilities”
添加
iCloud
功能,勾选Key-Value storage
✅ 总结体验 #
功能 | 技术 | 状态 |
---|---|---|
✅ SwiftUI 任务记录列表 | List + ObservableObject | 完成 |
✅ iCloud 同步记录 | NSUbiquitousKeyValueStore | 完成 |
是否还需要:
把任务记录导出为 CSV / 分享?
更高级的同步方式(使用 CloudKit + Core Data)?
添加搜索和筛选功能(比如只看“写作”记录)?
我可以继续帮你扩展这个系统,打造一款真正实用的极简专注工具。
非常好!我们继续为你的计时器项目增加以下功能:
✅ 新增功能目标 #
📤 导出任务记录为 CSV 文件,并支持系统分享(通过分享面板发送到 AirDrop、备忘录、邮箱等)
🧩 一、生成 CSV 数据 #
在 TimerHistoryStore.swift
中添加导出方法:
#
extension TimerHistoryStore {
func exportCSV() -> String {
var csv = "名称,时长(分钟),完成时间\n"
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm"
for record in history {
let line = "\"\(record.name)\",\(record.duration),\"\(formatter.string(from: record.completedAt))\""
csv.append(line + "\n")
}
return csv
}
}
🧩 二、创建 SwiftUI 分享面板(使用 ShareLink
)
#
添加导出按钮视图 #
import SwiftUI
import UniformTypeIdentifiers
struct TimerExportView: View {
@State private var csvData: Data?
@State private var isExporting = false
var body: some View {
VStack(spacing: 20) {
Text("导出计时任务记录")
.font(.title2)
.bold()
Button("生成并分享 CSV") {
let csvString = TimerHistoryStore.shared.exportCSV()
self.csvData = csvString.data(using: .utf8)
self.isExporting = true
}
if let csvData = csvData {
ShareLink(
item: csvData,
preview: SharePreview("任务记录.csv", image: Image(systemName: "square.and.arrow.up"))
) {
Label("通过分享发送", systemImage: "square.and.arrow.up")
}
.fileExporter(
isPresented: $isExporting,
document: CSVDocument(data: csvData),
contentType: .commaSeparatedText,
defaultFilename: "任务记录"
) { result in
switch result {
case .success:
print("导出成功")
case .failure(let error):
print("导出失败:\(error)")
}
}
}
}
.padding()
}
}
🧩 三、支持导出为文档类型 #
创建 CSVDocument.swift
(仅用于 fileExporter
)
#
import SwiftUI
import UniformTypeIdentifiers
struct CSVDocument: FileDocument {
static var readableContentTypes: [UTType] { [.commaSeparatedText] }
var data: Data
init(data: Data) {
self.data = data
}
init(configuration: ReadConfiguration) throws {
guard let data = configuration.file.regularFileContents else {
throw CocoaError(.fileReadCorruptFile)
}
self.data = data
}
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
return .init(regularFileWithContents: data)
}
}
🧪 示例整合 #
你可以将 TimerExportView()
加入设置页面或独立 Tab 页面:
TimerExportView()
.tabItem {
Label("导出", systemImage: "square.and.arrow.up")
}
✅ 总结 #
功能 | 实现方式 | 状态 |
---|---|---|
导出记录为 CSV | exportCSV() + DateFormatter | ✅ 已完成 |
分享面板 | ShareLink + fileExporter | ✅ 已完成 |
文件类型支持 | 自定义 CSVDocument | ✅ 已完成 |