从 SwiftUI 功能到 App Intents:设计系统级入口
App Intents 能把应用能力暴露给快捷指令、Siri、Spotlight 和其他系统入口。它不应直接调用某个 SwiftUI 按钮的闭包,也不应复制一份只在扩展环境中存在的业务逻辑。更可靠的结构是:SwiftUI 和 Intent 都把输入转换成同一个领域命令;领域层负责验证、鉴权、幂等与持久化;界面只呈现结果。
从一个窄而完整的动作开始
适合系统入口的功能应有明确动词、有限参数和可解释结果,例如“把任务标记为完成”或“记录一杯水”。“打开应用并做任何事”太宽,“把当前屏幕第三行设为绿色”又依赖界面上下文。先选一个不需要复杂交互、失败后可恢复的动作,再扩展组合能力。
import AppIntents
struct CompleteTaskIntent: AppIntent {
static let title: LocalizedStringResource = "Complete Task"
@Parameter(title: "Task")
var task: TaskEntity
func perform() async throws -> some IntentResult & ProvidesDialog {
let command = CompleteTask(id: task.id)
let result = try await TaskCommands.shared.execute(command)
return .result(dialog: "Completed \(result.title)")
}
}
Intent 只是适配层。TaskCommands 不应导入 SwiftUI 或依赖当前 view hierarchy。应用内按钮调用同一个 CompleteTask,因此权限、同步和错误规则不会产生两套版本。
AppEntity 是稳定引用,不是整张数据库记录
系统需要搜索和展示可选实体,但实体标识必须跨启动稳定,查询要有限且尊重账户边界。不要把敏感字段全部放进 AppEntity,也不要返回无限列表。显示名称可以本地化,真实主键用于执行时重新读取最新记录。
执行前再次验证实体存在、属于当前用户且仍允许操作。快捷指令可能在创建数月后运行,缓存的显示文本已经过期。若项目被删除或账户退出,返回可理解错误或要求打开应用,而不是崩溃或对错误账户执行。
区分后台可完成与必须打开应用
只有不需要额外确认、UI 选择或受保护上下文的操作才适合直接完成。支付、删除大量数据、改变分享权限等高风险动作应提供确认,并在必要时转入应用。不要为了追求“无界面”而绕过登录、生物识别或产品安全步骤。
Intent 运行环境的生命周期和前台应用不同。它可能没有现成的 SwiftData context、导航对象或内存缓存。把依赖通过明确容器创建,支持取消,并为网络调用设置合理超时。若数据尚未同步,返回真实状态而不是假装成功后等待未知后台任务。
可用性和降级要写在边界
如果应用仍支持 iOS 18,而某些 iOS 26 API 只在新系统存在,Intent 的核心命令应保持在共同版本上;新入口或参数通过 availability 分开声明。不要让旧系统加载包含新符号的路径。系统可能用不同界面呈现同一 Intent,因此不能假定按钮位置、颜色或特定对话框布局。
本地化包括标题、参数、实体名称、成功和失败对话。句子要在没有应用界面的情况下也能理解。VoiceOver 和语音调用需要短而明确的同义词,避免多个 Intent 使用几乎相同发音却产生不同副作用。
测试三层契约
领域命令使用普通单元测试覆盖权限、幂等、缺失记录和同步冲突。Entity query 用隔离数据仓库验证过滤、分页和账户边界。Intent 层只测试参数映射、对话和需要打开应用的分支,最后在真机用快捷指令与系统入口做少量端到端检查。
日志应关联 intent 执行标识和领域命令,但不能记录语音文本中的隐私内容。对于可重试命令使用幂等键,避免系统或用户重复触发产生两次记录。
结论
好的 App Intent 不是给 SwiftUI 页面加遥控器,而是把一个领域动作做成稳定系统接口。保持动作窄、实体引用稳定、执行前重新鉴权、危险副作用需要确认,并让前台 UI 与系统入口复用同一命令。这样新的入口增加可达性,却不会成为绕过应用规则的第二套后端。