1.

大模型的出现让翻译软件彻底完成了从「语言转换」到「语义理解」的范式转移,在 openai-translator 这类基于大模型的工具涌现之后,翻译应用的想象空间也被换算到了模型自身的发展进程。

但我仍然怀念极光词典

虽然词典和翻译并非是同一个使用场景,但于我自身的需求来说,大部分时候我仅仅只是需要快速查询词义,相比之下,再快的模型返回速度都感觉不够快。

与此同时,我也并不需要多么精确的表达和语气,结合上下文通常已经足够能理解。

当然更重要的一点是,Token 不是免费的,花费在这种低智力的事情上,我认为不够环保。

2.

我喜欢极光词典不贪婪的姿态,完全不做查词以外的任何功能。UI 即便不算精致但也安全稳定。它调用的是苹果系统词典,简洁纯粹,虽然这也是它被下架的原因。

很可惜的是,除此以外的同类产品,要么功能臃肿,要么毛坯到不堪入目。

macOS 上有独立的系统词典应用,但 iOS 上则需要长按菜单再通过点击才能调用,这个交互并不轻松,也不可能被所有应用支持。

所以制作一款 iOS 词典 App 一直以来都是我的小小愿望。

3.

在经历了网页和插件类产品的开发后,我意识到 AI 的上限显然远不止于此。是时候来尝试客户端产品了。

但作为词典,词库是首先需要解决的问题。不是指它的来源,因为 MIT 的词库并不少,但是他们的格式实在太不整齐了。

我粗略收集了十万余条词条数据,面对它们混乱的标点、不统一的词性格式、结构不同的释义… 额角还是流了几滴。

当然也考虑过直接让模型逐条理解和整理,理论上,它比正则更懂自然语言,也更适合处理模糊的异常。

但面对十万量级的数据造成的调用成本,以及考虑到终归还是要亲自检查,最后还是选择了较为朴素的清洗方式:我口述规则,AI 根据规则编写并执行 Python 脚本来批量处理。

例如,将释义中的半角逗号统一为全角逗号,修复错误的词性标记,处理 CSV 的引号和分隔符,甚至一些诸如使用半角括号加空格来代替全角括号的个人癖好。

听起来只是几个字符串操作,但实际上还是会不断发现例外、重新描述规则、检查结果,然后再次执行。

最终耗时近半个月,但也不排除仍有一些犄角旮旯没有被统一。

这是「编舟」的部分。

4.

仍然要赞美模型的强大,即使是 Gemini 2.5,也能顺利达成十万词条的毫秒级检索表现。

词库并不依赖 GRDB 或某种算法,仅仅是手写 SQLite 和 FTS5,并以只读方式随 App 一起打包。可能更应该赞美 SQLite。

实际上考虑到快速查询的场景,我在一定程度上精简了部分词条的释义,砍掉了音标、词形变化内容。

查询结果使用列表来展现,超过单行的部分我也决定尽量缩略,目的都是减小词库的体积,但最终词库仍有近 10MB。

为了兼顾查询速度和资源占用,数据库还需要跟随 App 的生命周期进行管理:进入后台时安全关闭,回到前台后自动重开并恢复查询。

5.

对于精简后的本地词库,它将只会为快速查询的场景服务。

而对于词义详情页面的构建,倒是经历了一点小波折。

在最初的版本里,为了复刻致敬极光词典的体验,一开始就决定将系统词典的释义作为详情页,一方面是零成本的诱惑,另一方面也只是天真地以为通过公开 API 调用完全合规。

然而审核被拒,意思是唯独词典类型的 App 不能调用它…

好吧,那也只能改用在线词典服务来提供详情页释义了。

6.

额外加入了桌面 Widget、锁屏入口和控制中心的快捷操作。这些并不难,算是为了进一步体验 iOS 开发的流程。

另外,我也增加了通过系统 Spotlight 搜索词条的功能,没想到的是,这个边缘功能的逻辑还挺复杂。

问题首先来自数据量。在无法保证 App 始终停留在前台的情况下,不适合把十万多个词条一次性交给 Core Spotlight,因此索引必须分批进行,并持续记录版本与进度。

用户可能随时锁屏、切换 App,系统也可能终止后台任务。为此,App 进入后台后会先通过 UIBackgroundTask 完成收尾;尚未完成的长任务,则交给 BGProcessingTask,等待系统在合适的时机重新唤醒。

更麻烦的是,App 记录的进度不一定等于 Core Spotlight 实际写入的进度。因此每批词条都会连同一份 client state 一起提交,让系统保存最后确认的索引检查点。App 重启后以它为准继续工作,同时处理版本更新、索引丢失、重建与修复。

最后,用户点击搜索结果时,还要正确打开词条详情,并恢复搜索框和键盘状态。

实在是有点费力。

7.

开发期间恰逢 WWDC 发布 iOS 26,App 还没发布就遇到了这种大考(。

然而由于 AI 这块知识储备还不足,适配过程还是走了点弯路。

比如设置页面的关闭按钮:

  • 起手直接使用 Button + Image(systemName: "xmark"),但是发现关闭操作时异常艰难,原因是点击区域只有可怜的 Symbol 部分。
  • 然后尝试了 Button(role: .cancel) + Label(systemImage: "xmark"),点击区域没问题了,但是 xmark 的尺寸断点和系统的关闭按钮并不一致 (偏小),并且「取消」的语义显然也是不正确的。
  • 紧接着尝试 UIViewRepresentable + UIButton + SF Symbol xmark,并用肉眼复刻了系统关闭按钮的 xmark 动态字号范围 (17–21 pt),点击范围大了一圈,但仍然没有完美覆盖。
  • 最后表现正确的方法是:Button(role: .close),把「关闭」的语义和样式交给系统处理,再使用 .topBarLeading 指定在左侧 (我想这么放)。

再比如官方似乎是不允许玻璃套玻璃的,容器使用了 glassEffect,内部的 glass 元素就会自动降级为纯色。这就导致如果我想为主题 / 图标增加付费选项时,当前实现下唤起的 Paywall 页面中的 Glass 元素就不能正常显示。为了保持这两个选项容器的简洁表现,最终只能忍痛放弃作为付费(。

另外在长期的测试过程中,我发现 xmark.circle.fill 作为 SF Symbol 调用时,视觉层面会往下偏移 0.5pt,强迫症如我只能手动 .offset(y: -0.25)

8.

还有一些可有可无锦上添花的功能:

  • 多主题和图标替换的花活,找回一些设计师的身份归属
  • 基于 iOS 自带的语音合成能力实现了英式 / 美式的单词发音功能
  • 滚动时的键盘和输入框行为的配置选项
  • 把「关闭搜索记录」当作了付费功能,我相信有人和我一样追求极致纯粹(
  • 一块钱的非核心功能付费纯当走一遍 StoreKit 配置流程

使用 SwiftUI 来构建,算是吃了一点没经验的亏,有太多犄角旮旯不能按预期表现,暴露的控制接口又十分有限,最后还是只能用 UIKit 重写局部组件。

这样比起来,写 Web 真的完完全全就是放松。

总之,比想象中还是多花了一点时间。