1. 为什么我选择用 Trae + SwiftUI 做单词本 Mac App
如果你正在找一个能在一小时内跑通的 macOS 原生小项目,单词本是个很合适的练手场景:数据模型简单、UI 结构清晰、增删改查逻辑完整,还能顺带把 SwiftData 的本地持久化跑一遍。Trae 负责把自然语言需求转成 SwiftUI 代码,SwiftData 负责数据落地,而 TaoToken 负责把模型调用的 Key 和 API 通道统一管起来,三者配合下来,一个可用的原型基本能在一次会话里成型。
这篇内容面向的是有基础 Swift 语法、但没怎么写过 SwiftUI 桌面端的人。我会把重点放在三件事上:Trae 里怎么配置 TaoToken 的统一 Key、SwiftData 的 Model 骨架怎么写、以及跑起来之后怎么验证增删改查真的生效。中间会给出可以直接复制的 config.toml 片段和 Model 定义,你照着改改就能用。
需要提前说明的是,Trae 本身是编辑器侧的 AI 编程工具,TaoToken 在这里扮演的是模型调用通道的角色,两者是配合关系,不是替代关系。你仍然需要自己理解生成的代码,尤其是 SwiftData 的 @Model 宏和 ModelContainer 的注入位置,这两处出错的话编译能过但运行会崩。
2. TaoToken 前置准备:统一 Key 与 API 通道
在 Trae 里调用模型,绕不开配置 API 地址和 Key。TaoToken 的做法是把多个模型的调用收敛到一个 Key 上,这样你在 Trae 的配置文件里只需要维护一份凭证,切换模型时改模型名就行,不用来回换 Key。
先到控制台创建一个 API Key。打开 https://taotoken.net/console ,登录后在 API Keys 页面点新建,复制生成的 sk- 开头的字符串。这个 Key 只在创建时完整显示一次,建议先存到密码管理器里。
拿到 Key 之后,需要确认两件事:一是 API 的基础地址,二是你要用的模型名。基础地址是 https://taotoken.net/api ,注意这个地址不带任何查询参数,直接作为 base_url 使用。模型名可以在模型对话页面里查看当前可用的列表,选一个你熟悉的就行。
注意:API Key 属于敏感凭证,不要直接提交到 Git 仓库。建议放在本地配置文件里,并把该文件加入 .gitignore。
如果你后续要做长期的编码任务或者 Agent 类的自动化流程,可以了解一下 Coding Plan,它在调用额度和并发上会比按量计费更适合高频场景。入口在 https://taotoken.net/coding-plan 。
3. 在 Trae 中配置 config.toml 的完整片段
Trae 的模型配置走的是 config.toml 文件。不同版本的 Trae 配置文件位置略有差异,一般在用户目录下的 .trae 文件夹里,或者通过设置界面的「打开配置文件」直接跳转。找到之后,把下面这段贴进去,替换掉你自己的 Key。
# Trae 模型通道配置 # 统一走 TaoToken,切换模型只改 model 字段 [provider.taotoken] type = "openai-compatible" base_url = "https://taotoken.net/api" api_key = "sk-你的Key粘贴在这里" [model.default] provider = "taotoken" model = "claude-sonnet-4-20250514" max_tokens = 8192 temperature = 0.2 [model.fast] provider = "taotoken" model = "gpt-4o-mini" max_tokens = 4096 temperature = 0.3几个参数说明一下。type 用 openai-compatible 是因为 TaoToken 的接口兼容 OpenAI 的请求格式,Trae 能直接识别。temperature 在写代码场景下建议调低,0.2 左右比较稳,太高的话生成的 SwiftUI 代码容易在细节上飘。max_tokens 给到 8192 是为了让 Trae 一次能吐出完整的 View 文件,不然长文件会被截断,你还得手动拼接。
配置改完记得重启 Trae,或者在设置里点一下「重新加载配置」。验证配置是否生效最简单的办法是在对话流里问一句「当前使用的模型是什么」,如果返回的模型名和你 config.toml 里写的一致,说明通道通了。
如果你更习惯用命令行工具做接入调试,可以参考接入文档里的示例,用 curl 直接打一次请求,确认 Key 和地址都没问题再回到 Trae 里操作。文档地址在 https://taotoken.net/doc 。
4. SwiftData Model 骨架与 SwiftUI 视图搭建
配置通了之后,回到项目本身。先在 Xcode 里新建一个 macOS App,Interface 选 SwiftUI,Language 选 Swift,勾选 Use SwiftData。项目建好后用 Trae 打开整个文件夹。
第一步是定义 Word 模型。在 Trae 对话流里用 #Folder 选中项目根目录,把下面这段需求描述贴进去:
import Foundation import SwiftData @Model final class Word { var title: String var isError: Bool var isMaster: Bool init(title: String, isError: Bool = false, isMaster: Bool = false) { self.title = title self.isError = isError self.isMaster = isMaster } }@Model 这个宏是 SwiftData 的核心,它会自动帮你生成持久化所需的代码。注意类要用 final 修饰,这是 SwiftData 的推荐写法,能避免继承带来的元数据问题。三个属性都是存储属性,SwiftData 会自动把它们映射到本地数据库的字段。
接下来是数据初始化。在 App 的入口文件里注入 ModelContainer,并生成 50 条测试数据:
import SwiftUI import SwiftData @main struct WordBookApp: App { var sharedModelContainer: ModelContainer = { let schema = Schema([Word.self]) let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false) do { return try ModelContainer(for: schema, configurations: [config]) } catch { fatalError("ModelContainer 创建失败: \(error)") } }() var body: some Scene { WindowGroup { ContentView() } .modelContainer(sharedModelContainer) } }这里有个容易踩的坑:ModelContainer 的创建如果失败,用 fatalError 直接崩掉比静默失败要好,因为 SwiftData 的 schema 不匹配时错误信息很隐晦,早点暴露问题反而省时间。
然后是 ContentView 的三栏结构。侧边栏三个按钮切换右侧列表,右侧根据选中状态展示不同过滤条件的数据。核心逻辑用 @Query 配合 predicate 实现:
import SwiftUI import SwiftData enum SidebarItem: String, CaseIterable, Identifiable { case all = "单词本" case error = "错词" case master = "已掌握" var id: String { rawValue } } struct ContentView: View { @Environment(\.modelContext) private var context @Query private var words: [Word] @State private var selection: SidebarItem = .all var body: some View { NavigationSplitView { List(SidebarItem.allCases, selection: $selection) { item in Text(item.rawValue).tag(item) } } detail: { switch selection { case .all: WordList(words: words.filter { !$0.isMaster }) case .error: WordList(words: words.filter { $0.isError }) case .master: WordList(words: words.filter { $0.isMaster }) } } .onAppear { if words.isEmpty { generateWords() } } } private func generateWords() { let samples = ["apple", "banana", "cherry", "dragon", "eagle", "forest", "garden", "harbor", "island", "jungle"] for i in 0..<50 { let word = Word(title: samples[i % samples.count] + "\(i)") if i < 10 { word.isError = true } if i >= 10 && i < 15 { word.isMaster = true } context.insert(word) } try? context.save() } }注意 generateWords 的调用位置。我试过直接写在 body 里,编译能过但运行时会报「Modifying state during view update」的警告,正确做法是放在 .onAppear 里,让它在视图加载完成后再执行。
5. 运行验证:增删改查是否真的生效
代码写完之后,按 Cmd + R 跑起来。第一次启动时数据库是空的,onAppear 会触发 generateWords,插入 50 条数据。你应该能看到侧边栏三个选项,右侧默认显示单词本列表。
验证增删改查分四步走:
第一步,点单词本里任意一条的「添加到错词」按钮,然后切到错词标签页,看这条数据是否出现在列表里。这一步验证的是 isError 字段的更新和 predicate 过滤是否生效。
第二步,在错词页点「已掌握」,再切到已掌握页,确认数据流转正确。这里涉及两个字段同时更新:isError 置 false,isMaster 置 true。
第三步,在已掌握页点「移除」,数据应该回到单词本列表。这一步验证的是反向更新。
第四步,关掉 App 再重新打开,数据应该还在。如果重启后数据丢了,说明 ModelContainer 的 isStoredInMemoryOnly 被设成了 true,检查一下配置。
如果某一步没生效,先看控制台有没有 SwiftData 的报错。常见的是 predicate 里用了不支持的语法,比如对 Bool 字段做复杂比较。SwiftData 的 #Predicate 宏对表达式有限制,简单过滤建议在内存里用 filter 做,就像上面代码里那样。
6. 本篇常见错误排查
编译报错「Cannot find 'Word' in scope」:检查 Word.swift 文件是否加入了当前 target。在 Xcode 的 File Inspector 里看 Target Membership 有没有勾上。
运行时崩溃「ModelContainer 创建失败」:多半是 schema 变了但旧数据库还在。删掉 App 的容器目录重新跑,路径在 ~/Library/Containers/ 下对应 bundle id 的文件夹里。
Trae 生成的代码里 if 和 ! 之间多了空格:这是模型输出的格式问题,Swift 编译器对if!word.isMaster这种写法会报错。手动改成if !word.isMaster即可,或者在 Trae 里追加一句「注意 Swift 语法中 if 和 ! 之间需要空格」。
@Query 拿到的数据不刷新:确认 modelContext 是从 @Environment 注入的,而不是自己 new 了一个。SwiftData 的上下文必须和 ModelContainer 绑定才能触发视图更新。
TaoToken 请求返回 401:检查 config.toml 里的 api_key 有没有多余空格,以及 base_url 是否写成了带路径的形式。正确写法就是 https://taotoken.net/api ,不要在后面加 /v1 之类的后缀。
7. 下一步:把原型变成日常工具
跑通之后,你可以在这个骨架上继续加东西。比如给 Word 加一个 createdAt 时间戳字段,按添加时间排序;或者加一个搜索框,用 @Query 的动态 predicate 做实时过滤。Trae 在这类增量需求上表现不错,用 #File 选中对应文件,描述清楚要改什么,基本一次就能改对。
如果你打算把这个 App 长期用下去,建议把模型调用也接进来,比如加一个「自动生成例句」的按钮,走 TaoToken 的模型对话接口拿返回结果。这样单词本就不只是记录,还能帮你扩展用法。模型对话的入口在 https://taotoken.net/models ,可以先在网页上试好 prompt 再写进代码。
Key 的管理上,如果你同时用 Trae 做其他项目,建议在 TaoToken 控制台里按项目建不同的 Key,方便追踪用量和随时吊销。API Keys 管理页在 https://taotoken.net/api-keys ,新建的时候备注一下用途,后面排查问题会省事很多。
最后提醒一句,Trae 生成的代码一定要自己过一遍。它在大结构上很少出错,但细节上偶尔会飘,比如前面提到的 if 空格问题、onAppear 位置问题。把它当成一个打字很快但需要你 review 的搭档,而不是完全放手不管的黑盒,这样效率和质量都能兼顾。