Skip to content
lawrence1912Public

About

macOS 多功能外壳:SwiftUI + Swift 6 严格并发 + 多模块 SwiftPM。装了七个开发者日常工具和一块卡片式备忘板。

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

5 Commits

Folders and files

Repository files navigation

LetItGo · 百宝箱

一个 macOS 百宝箱:把每天零碎要用的小工具收进同一个窗口。

转个 Base64、确认那串数字是秒还是毫秒、把 JWT 拆开看一眼过期没有、 生成一串够长的密码、把 Java 那边给的 PEM 换成 Apple 认的格式 —— 这些事单独拿出来,一件都不值得开一个网页,更不值得各装一个应用。 它们的共同点是打断:正写着代码,为了三秒钟的换算跳出去一趟。

所以有了这个箱子。⌘1–⌘9 直达,⌘, 调设置,用完 ⌘W 关掉。

所有计算都在本机跑,一个字节都不往外发。 —— 生产环境的 token 粘进在线解析工具,等于把凭证交给了别人的服务器。

技术栈:SwiftUI · Swift 6 严格并发 · @Observable · SwiftPM 多模块 · swift-testing。 176 条用例,make run 一条命令出 .app。


箱子里装了什么

分区 它替你省掉的那件事
⌘1 概览 能用的分区摊成入口卡片:图标 + 名字 + 键盘编号。留白分区不列 —— 点进去只有「功能还没接」的卡片不是入口,是死路
⌘2 待办 双列卡片板:左边「待办」,右边「已完成」。顶上那行输入框常驻,打字、回车就加上了(没有 sheet,也没有「标题 / 正文」两栏);卡片直接拖到另一列,或者点卡上的方框;× 删掉。落盘 JSON,重启还在
⌘3 编解码 Base64(标准 / URL-safe)、URL 百分号、十六进制。解码宽进:少了 =、两套字母表混用、带换行或 0x 前缀,全都收
⌘4 时间戳 epoch ↔ 日期。秒还是毫秒按量级自动判断,并且把判断结果写在界面上(「按毫秒读」)—— 猜错三个数量级,日期会落到 1970 年,而且看着还挺像
⌘5 哈希 MD5 / SHA-1 / SHA-256 / SHA-512 一次全算,UTF-8 取字节
⌘6 随机串 密码 / 密钥 / 测试数据,一次给 10 个(挑一个顺眼的,逐行复制或整批拿走)。随机源是 CSPRNG,对应 Java 的 SecureRandom,不是 Math.random()
⌘7 JSON 格式化 / 压缩 / 转成 Java 字面量。转义过的 JSON(引号是 \")和中文引号都会自动认出来并修好,修了什么逐条写在界面上
⌘8 JWT 拆开看 header / payload,exp iat 换算成能读的时间并标出过期没过期,HS256/384/512 可验签
⌘9 RSA 生成密钥对、加解密。麻烦的从来不是加解密,是密钥格式:Apple 只认 PKCS#1,Java 给的是 X.509 / PKCS#8 —— 中间那层 DER 壳子这里自己拆自己包,进出都是 Java 直接能用的 PEM

「活动」那格是刻意留白,不是没做完 —— SidebarItem.isPlaceholder 把这件事 写成了代码,概览页据此跳过它,加新分区时编译器会逼你表态。

三个不肯将就的地方

  • 宽进严出。 粘进来的东西是从聊天窗口、日志、PPT 里捞出来的,带着换行、 中文引号、多余的前缀。工具的职责是认出它们,不是让你先手动清理一遍。
  • 把判断说出来。 自动判断很方便,猜错了很致命。所以「按毫秒读」「修好了 什么」这类话直接写在界面上 —— 悄悄帮你修好是最难查的一类行为。
  • 不动内容,只动语法。 JSON 里字符串内部的全角逗号一个字不换: 那是数据不是语法,换掉它 JSON 依然合法,但你的数据被悄悄改了。

长什么样

不是一个默认的 AppKit 窗口:一颗能量星球铺在主窗口底部, 窗口 chrome、控件和卡片是压在它上面的系统 Liquid Glass(macOS 26)。

  • 六套色板(棱镜 / 星云 / 霜白 / 莫兰迪 / 雾青 / 素白,默认棱镜),设置里随时切;每个色写成 OKLCH 的 四组值(浅 / 深 / 增强对比度浅 / 深),绘制时按当前外观挑。换主题换的是整个界面 —— 连边栏的选中块、滑块、勾选框、聚焦环这些系统控件一起换。
  • 默认那套是「棱镜」:最主流的那套科幻蓝 —— 近黑深蓝打底,主操作是电光青, 青 / 电蓝 / 靛紫组成的全窗口能量星球不占页面布局空间。 「星云」是暖调深空 + 琥珀(还多一层 40pt 的细网格);想安静就换霜白 / 素白。
  • 星球是个光源,不是一张贴图:整颗球服从一个方向 —— 光从左上打过来, 能量环在那一侧烧成一道近白的热弧,绕回右下衰减成微光;球体本身是暗的, 左上一道贴着球面的边缘光交代它有体积。
  • 它也不只是好看:glassEffect 折射的是背后真实存在的内容, 背后什么都没有的玻璃只会渲染成一块灰板。所以那颗星球和那层细网格是这套材质的 前提 —— 它们提供了可折射的起伏和可对齐的规律。
  • 四档材质,不是「什么都做成玻璃」:近黑的窗口场 → 氛围(星球 + 细网格) → 烟熏面(大卡:系统材质 + 压沉的幕 + 细边 + 投影,不上玻璃) → 交互玻璃(选中的导航项、分段控件、按钮)。 分界线是:玻璃是给控件和导航的;一张大卡的活儿是沉下去、把注意力让给里面的字。
  • 补的两层光浓度差一个数量级:斜光(白 0.07,压在正文底下,进对比度合成链) 和镜面边(1px,可以亮到 0.85,正文压不到它)。 这两个数是从「一排塑料糖豆」那一版收回来的 —— 光该是察觉不到的一层。
  • 阅读区使用稳定底色:输入框、代码和结果区隔开背景纹理,清透玻璃留给导航和短控件。
  • 换主题保留当前编辑状态:颜色更新会触发重绘,输入、选项和撤销记录不会随主题清空。
  • 按钮是自己画的,但只画:ButtonStyle 只接管画法(点击 / ⏎ / Tab 焦点 / 禁用传播一样不丢),底下那层材质仍然是系统的 glassEffect。 在它上面补三样系统不给的东西 —— 顶面那道亮带、一圈有方向的镜面边、 主操作底下漏出的辉光。
  • 一屏只有一个响亮的东西:只有主操作发光;「当前选中」低一档;别的不上色。 破坏性操作不发光 —— 辉光是在邀请人按它,它要好认不该好按。
  • 对比度是算过的,但这一版算得比上一版少:系统材质怎么混不该由我们假设, 所以测试退回到还归自己画的两块底 —— 氛围场(下界)和读写面(精确)。 正文 ≥ 4.5:1,增强对比度那两组 ≥ 7:1,有测试兜着。
  • 无障碍四项全要:减弱动态、减弱透明度、增强对比度、完整键盘 + VoiceOver。 这一版大部分交还给了系统控件 —— 还给系统的那些,白拿上下键遍历、 首字母跳转、.isSelected 语义和减弱透明度的自动让位。

完整规范见 DESIGN.md,产品原则见 PRODUCT.md。 DESIGN.md 文末记了八个踩过的 SwiftUI 坑,前三条都是「编译通过、看不出错、 就是不工作」那种,其中两条能把整个窗口搞坏 —— 动界面之前先看那几条。


上手

make run      # 编译 → 打包成 .app → 启动
make test     # 跑测试
make help     # 看全部命令

要求 macOS 26+ 和 Swift 6 工具链(Liquid Glass 是 26 才有的)。只装 Command Line Tools 也能跑, 不需要完整 Xcode(代价见文末)。

产物在 build/LetItGo.app,可以直接双击。待办的数据落在 ~/Library/Application Support/com.lawrence.LetItGo/items.json。


往箱子里再放一件工具

这个箱子的重点不是现在装了七件,是再装一件不用重新做决定:

  1. 建 Sources/FeatureFoo/,写 FooModel.swift(@MainActor @Observable) 和 FooView.swift,照 FeatureHome(有存储)或 FeatureToolbox(纯函数)抄。
  2. Package.swift 加一个 target,挂到 LetItGo 的 dependencies 上。
  3. AppCore/Navigation.swift 的 SidebarItem 加一个 case。
  4. 编译 —— DetailView 的 switch 是穷尽的,编译器会报错提醒你把新界面接上。 这一步是故意的:不会出现「加了菜单但点进去是空白」。
  5. 新界面如果有「主操作」,用 .focusedSceneValue(\.newItemAction, …) 登记一下, 侧边栏 +、工具栏 +、⌘N 三个入口立刻对它生效,不用改壳。

「工具」那一组的逻辑层是纯函数:没有网络、不碰磁盘、没有依赖,所以整个可测 —— 测试全打在 Codec / TimeConversion / Digest / RandomString / JSONFormatter / JWT / RSA 这七个 enum 上,界面层只负责摆。不好测的东西一律做成参数: 随机串把随机源做成参数(测试传定种子的),时间相关的一律把 now 做成参数 —— 于是「每类至少一个」「已过期 4 小时」这些性质可以真的断言。


里面怎么搭的

                    LetItGo (app shell)
                   ┌────────┴─────────┐
                   │  Scene / 路由 /   │   ← 只做接线,不写业务
                   │  菜单 / 组合根     │
                   └────────┬─────────┘
          ┌─────────┬───────┴───────┬──────────┐
          ▼         ▼               ▼          ▼
  FeatureHome  FeatureToolbox  Persistence  DesignSystem  AppCore
       │            │               │            │           ▲
       └────────────┴───────────────┴────────────┴───────────┘
                          依赖单向,都指向 AppCore
模块 职责 不该放什么
AppCore 领域模型、AppState、导航枚举、依赖容器、Repository 协议 具体存储实现、界面
DesignSystem 设计令牌(间距 / 圆角 / 色阶 / 字号 / 动效)、组件、按钮样式 业务逻辑
Persistence ItemRepository 的实现(落盘 JSON + 内存两个 actor) 界面、领域规则
FeatureHome 待办板:HomeModel + HomeView + 列 + 卡片。有存储的功能照它抄 别的功能的东西
FeatureToolbox 七个工具一个模块 —— 它们共用同一套「输入 → 选项 → 结果」骨架 别的功能的东西
LetItGo 入口、Scene、侧边栏 / 详情路由、菜单、组合根 任何业务逻辑

依赖图写在 Package.swift 里,编译器强制单向:AppCore 引用不到任何人。

五条贯穿全项目的约定

  1. 状态用 @Observable,不用 ObservableObject —— 视图只因真正读到的属性重绘。
  2. 依赖从 init 注入,唯一 new 具体实现的地方是 App/AppDependencies.swift; 测试里换掉某一项即可(见 Tests/FeatureHomeTests 的 FailingItemRepository)。
  3. 只有跨界面共享的状态才进 AppState,单个界面自己的状态属于它自己的 Model。
  4. 读吞错,写抛错。 load() 失败就地显示错误态(反正整屏没内容,再弹个 alert 只是让人多点一次「好」);create / delete 失败抛出去,由壳弹全局 alert —— 写操作是用户主动发起的,失败必须打断他。
  5. 界面外的入口走 focused value,不走全局状态。 界面用 .focusedSceneValue 登记自己能做什么,侧边栏 / 工具栏 / 菜单用 @FocusedValue 读。没人登记时是 nil, 入口自动变灰 —— 「⌘N 在概览变灰」「⌘⌫ 没选中时变灰」这两条行为, 代码里没有任何一处 if 在判断分区或选中数。

换掉存储实现只需改 AppDependencies.live() 一行,上层一个字不动 —— Tests/AppCoreTests/FileItemRepositoryTests.swift 那几条用例是写给协议的, 换实现时原样复用就能验证新实现。


常用命令

命令 作用
make build 编译(debug)
make run 打包 + 启动
make app 只打包到 build/LetItGo.app(SANDBOX=1 带沙盒权限)
make app-release release 版 .app
make test 跑测试
make clean 清产物
make xcode 生成 .xcodeproj(需 Xcode + xcodegen)

标志是代码画的:Resources/Logo/mark.svg 是本体,应用图标由 Scripts/make_icon.swift 生成(改造型只需改脚本里的几何参数再重跑), Scripts/bundle.sh 会把它装进 .app。


当前环境的限制

开发这个项目的机器只装了 Command Line Tools,没有 Xcode。三个编译器宏插件 只随 Xcode 分发,所以做了对应处理 —— 装上 Xcode 后这些都可以换回标准写法:

用不了 这里的替代 装 Xcode 后
@Entry 手写 EnvironmentKey 可换成 @Entry,行为一致
@Model(SwiftData) Repository 协议 + 落盘 JSON actor 见 Persistence/PersistenceNotes.swift
#Preview 用 #if ENABLE_PREVIEWS 包起来 Xcode 工程会定义这个宏,预览自动生效
actool(.xcassets) 色板写在代码里;图标由脚本直接画成 .icns 可以改用资源目录

Makefile 会自动检测 developer dir:切到完整 Xcode 之后,make test 里那几个 给 swift-testing 补搜索路径的参数会自动消失。

project.yml(XcodeGen 那条路)尚未在真机验证过 —— 本机装不了 xcodegen。 SwiftPM 这条路是完整跑通的。


目录

letitgo/
├── DESIGN.md                # 视觉规范(颜色 / 密度 / 组件 / 动效 / 六个坑)
├── PRODUCT.md               # 产品原则与无障碍要求
├── Package.swift            # 模块依赖图
├── Makefile                 # 命令入口
├── Sources/
│   ├── AppCore/             # 领域 + 状态 + DI 契约
│   ├── DesignSystem/        # 令牌 + 组件 + 六套色板
│   ├── Persistence/         # ItemRepository 实现
│   ├── FeatureHome/         # 待办板(双列 + 拖放)
│   ├── FeatureToolbox/      # 编解码 / 时间戳 / 哈希 / 随机串 / JSON / JWT / RSA
│   └── LetItGo/             # 应用壳(入口 / 路由 / 菜单 / 设置)
├── Tests/                   # 176 条 swift-testing 用例
├── Resources/               # Info.plist / entitlements / 图标 / 标志
└── Scripts/                 # bundle.sh(→ .app)、make_icon.swift(→ .icns)

About

macOS 多功能外壳:SwiftUI + Swift 6 严格并发 + 多模块 SwiftPM。装了七个开发者日常工具和一块卡片式备忘板。

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages