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 真的完完全全就是放松。
总之,比想象中还是多花了一点时间。