Featured image of post 我和 Codex 把一个包裹列表做成了 ParcelBoard:从 SwiftUI 到灵动岛与 Apple Watch

我和 Codex 把一个包裹列表做成了 ParcelBoard:从 SwiftUI 到灵动岛与 Apple Watch

一个最初只会记录运单的 iOS 小项目,怎样在不断真机测试和返工中,长成带地图、实时活动、小组件与手表端的 ParcelBoard。

ParcelBoard 最早并不是一个功能完整的快递 App。第一版能做的事情很少:添加一个包裹,在列表里看状态,点进去看几条物流动态,不需要的就删掉。

真正开始在手机上使用以后,问题很快多了起来。单号从哪里来,能不能直接扫运单,状态怎样自动更新,取件码放在哪里最顺手,锁屏和 Apple Watch 能不能看,物流节点又该怎样画到地图上。每补上一块,工程都会牵出新的系统限制和交互问题。

截至 2026 年 8 月 28 日,iOS 工程已经来到 3.1.12 (35)。它仍然是一个没有上架的测试项目,但已经从本地列表变成了可以完整走通日常流程的原生 SwiftUI App。

ParcelBoard 当前主界面,画面使用本地演示包裹

我为什么想做一个包裹管理 App

我想要的并不只是“查一次快递”。我更希望有一个自己能控制的界面,把运单号、当前状态、最近动态、地图节点和取件码放到一起。打开以后先看到哪些包裹还在运输、哪些今天派送、哪些需要注意,而不是每次重新复制单号再查询。

iOS 本身也有很多适合这类信息的入口。桌面小组件适合看概览,实时活动和灵动岛适合盯一个正在派送的包裹,Apple Watch 适合抬腕确认状态。取件码如果能从快捷指令写入,就不必临时翻短信。

这些想法单独看都不复杂,放进一个工程后却会互相影响。主 App 保存的数据要同步给小组件和手表,物流更新要同时刷新通知与实时活动,点击锁屏卡片还要准确回到对应的包裹详情。ParcelBoard 后面的开发,主要就是在处理这些连接处。

第一版先把最小闭环跑通

1.0.0 只做了四件事:包裹列表、状态筛选、物流动态和设置。数据都保存在本地,添加、查看、删除能够正常工作,就算完成第一步。

这个阶段看起来朴素,却很有必要。如果一开始就同时接接口、做地图、加组件和手表,很难判断一个问题究竟出在数据、界面还是跨进程同步上。先让本地模型稳定下来,后面的功能才有可以依靠的底座。

到了 2.1.0,我开始在真机上调整空列表、状态筛选和详情交互,同时给新版工具栏准备布局,并保留 iOS 18 的兼容路径。

ParcelBoard 2.1.0 的早期真机设置页

现在回看,早期版本和今天的外观差别没有想象中大。变化最大的不是颜色或圆角,而是数据从哪里来,以及同一份包裹状态要送到多少个系统入口。

从演示数据走到真实物流

2.2.0 开始加入数据服务选择,2.3.0 接入快递100真实查询。工程里目前保留四种模式:本地演示、快递100直连、自建 HTTPS 代理,以及受控测试使用的专用服务 Beta。

演示模式一直没有删。它不仅方便截图,也能在不消耗接口次数、不暴露真实单号的情况下测试状态变化、异常包裹、跨城路线和通知。开发者选项还可以忽略本地限频、模拟网络延迟、显示数据来源和导出诊断信息。

ParcelBoard 开发者选项中的演示模式与查询调试开关

真实查询比发送一个运单号麻烦。不同快递公司需要不同的承运商编码,顺丰和中通等查询有时还需要手机号。返回的数据也不一定完整,状态、时间、地点和经纬度都有缺失的可能。App 需要把这些结果整理成统一模型,再决定该不该触发通知、更新实时活动或者重绘地图。

为了避免无意中连续调用接口,我给同一运单加了 30 分钟的本地查询保护。手表端也不会直接查询快递服务,只接收 iPhone 已经保存的快照,因此不会因为抬腕或点同步而额外消耗 API 次数。

这里还有一笔不能忽略的长期成本。快递100的实时快递查询接口是按查询量使用的付费服务。现在个人测试的调用量还容易控制;如果以后用户变多,就需要购买更多查询额度或企业套餐,价格以官方后台当时显示的方案为准。到那一步,服务器代理、预算上限、缓存和防滥用就不再是“以后再做”的优化,而是能不能持续提供服务的前提。

授权信息保存在设备钥匙串里,但这不等于可以把共享密钥直接放进公开版本。个人真机测试可以直连,真正公开分发时更合适的做法是把服务商凭据留在服务器,通过自己的 HTTPS 代理转发请求。

添加包裹不能只靠手动输入

如果每个运单都要逐字输入,这个 App 很快就会变成一个不想打开的工具。因此后面的版本逐步加入了几条入口:

  • 手动填写单号、快递公司和名称。
  • 用相机扫描条形码。
  • 从运单照片里同时识别条码和印刷文字。
  • 回到前台时检查剪贴板里的单号,确认后进入添加页。

这里最容易误判的是权限。iOS 对相机、照片和剪贴板有各自的系统机制,App 不能假装已经获得权限,也不应该一启动就读取所有内容。ParcelBoard 把识别动作放在用户能看见的入口里,并在首次引导中解释为什么需要这些权限。

照片识别也不是简单 OCR。运单上可能同时出现订单号、手机号、地址和多个条码。识别结果必须先筛选格式,再交给用户确认,不能看到一串数字就直接加入包裹列表。

地图画的是物流节点,不是快递车的位置

地图是这个项目里返工最多的部分之一。

快递100在部分结果里会返回行政区中心坐标,自建服务也可以提供经纬度。旧数据只有地点文字时,App 会尝试用系统地理编码补出坐标,然后用 MapKit 把有效节点连起来。

这条线只能表达“包裹经过了哪些物流节点”。它不是车辆 GPS,更不是严格沿道路行驶的路线。为了不把示意图做得像实时追踪,我在只有两个节点时只生成轻微弧度,多节点使用平滑样条减少尖锐折线,并在界面和说明里保留这个边界。

到了 3.1.5,详情页开始参考 Apple 健身 App 的层次:地图成为整页视觉底层,标题和物流卡片从渐变中自然进入;横屏则改成全屏地图加左侧半透明信息面板。这个方向确定以后,仍然连续改了七个小版本。

有时是地图容器跟着滚动缩放,有时是内容穿出玻璃面板,有时是灵动岛安全区读成零,还有一次只是标题和卡片之间多出了一截很明显的空隙。3.1.12 最后处理的是横屏返回按钮与面板触控区域重叠。这样的 Bug 在模拟器里不一定明显,换到真机横屏、下拉和旋转以后才会出现。

ParcelBoard 3.1.12 横屏地图详情,路线与包裹均为演示数据

实时活动不是缩小版详情页

我一开始也想把更多物流信息塞进灵动岛,结果很快遇到文字裁切、安全区和载荷大小问题。后来按照 Apple 的设计方式,把实时活动拆成锁屏、紧凑、最小和展开四种形态。

紧凑状态只保留图标和配送进度,展开后才显示起点、运输、终点以及一条关键动态。点击活动会通过深层链接直接打开对应包裹,而不是只回到 App 首页。

Apple 要求实时活动的静态数据和动态数据合计不能超过 4 KB,而且扩展本身不能联网或持续读取位置。ParcelBoard 因此只把必要状态交给 ActivityKit,迷你路线在本地根据物流节点绘制。布局结构发生变化时,还需要提高布局版本并重建旧活动,否则系统可能继续显示缓存中的旧版空白内容。

这部分后来比预计多花了很多时间。锁屏卡片能显示,不代表紧凑灵动岛不会被圆角裁掉;新安装正常,也不代表从旧版本升级后不会留下旧活动。真正稳定的标准,是四种形态、深浅色、升级路径和点击跳转都能在真机上走通。

ParcelBoard 在锁屏上显示的派送中实时活动,内容为演示数据

Apple 的 Live Activities 设计规范 强调“快速扫一眼就能理解”,ActivityKit 文档 也明确说明了 4 KB 限制和扩展不能联网。这两条最后直接改变了 ParcelBoard 的布局和数据结构。

取件码为什么用了快捷指令

取件码看起来像一条短信里的几个字符,但普通 iOS App 不能直接扫描系统短信数据库。与其做一个无法正常工作的“自动读取”,我最后选择了快捷指令。

用户可以让快捷指令接收短信内容,提取取件码后写入 ParcelBoard。App 再把它同步到包裹详情、最近动态、实时活动、灵动岛、小组件和 Apple Watch。这样系统边界清楚,自动化过程也由用户主动安装和授权。

我把目前使用的快捷指令放在这里:安装 ParcelBoard 取件码快捷指令。安装前仍然应该逐步检查动作,确认它只处理自己愿意交给 App 的内容。

小组件与 Apple Watch 共用一份快照

ParcelBoard 的小组件支持不同尺寸,显示包裹概览或最近动态。主 App 会把经过整理的快照写入 App Group,Widget 扩展只负责读取和展示。这样小组件不需要复制整套数据库和网络层。

Apple Watch 采用相似思路。iPhone 通过 WatchConnectivity 发送已保存的包裹快照,手表保存最近一次结果,因此手机暂时不在附近时仍能查看。手表首页只放在途、派送中、异常和最近包裹,点进详情再看位置、预计送达和最近几条物流动态。

Apple 对 Watch Connectivity 的定位就是在配对的 iPhone 与 Apple Watch 之间传递少量数据或文件。对这个项目来说,同步快照比在手表端重新建立网络查询更简单,也更节制。

一个列表点击问题,连续出了两个紧急版本

3.0.0 加入 Apple Watch、取件码和新版实时活动以后,我又把列表交互改回系统原生导航,并希望保留 iOS 的双指连续多选。

问题出在 SwiftUI 的 List(selection:)。选择绑定一直存在时,普通单击会被选择逻辑抢走,用户点包裹却进不了详情。3.0.1 先修了一次,真机上仍然不彻底,于是 3.0.2 直接在普通状态停用列表选择绑定,双指手势再单独开启多选。

这件事很能代表 ParcelBoard 的开发过程。代码看起来符合系统组件的用法,预览里也没有报错,但真实手指、导航动画和多选状态叠在一起,行为才会变得不一样。后来我不再尝试用一套状态同时承担导航和选择,而是把两种交互明确分开。

后台更新有系统边界

App 可以在回到前台时刷新,也可以注册后台刷新任务并发送本地通知,但这不代表 iOS 会在每条物流更新出现的那一秒唤醒它。后台任务何时运行由系统决定,用户设置、使用习惯、电量和网络状态都会影响调度。

所以 ParcelBoard 当前的本地方案适合“系统认为合适时检查”,不能承诺即时推送。要做到服务端一有变化就更新,需要自己的服务器定期查询,再通过 APNs 或 ActivityKit 推送把状态送到设备。Apple 的 Background Tasks 文档 也把这类能力描述为由系统调度的后台处理,而不是常驻进程。

真正卡住发布的,不是代码

工程能够在自己的设备上运行,不等于已经具备上架条件。我在注册 Apple Developer Program 时遇到提示:一台受信任设备已经和现有开发者账户关联,因此注册无法继续。没有完成开发者计划注册,我就无法走正式签名、TestFlight 和 App Store 发布流程。

我已经联系 Apple Developer Support。支持人员说明,他们出于隐私原因无法告诉我这台设备关联的是哪个账户,只能请我登录 Apple 账户安全页面,检查受信任设备列表并移除不属于自己的设备。我照着步骤检查并继续沟通过,但截至写这篇文章时,问题仍然没有解决。

Apple Developer Support 对受信任设备关联问题的说明,案例编号与个人信息已裁去

所以现在没有 ParcelBoard 的 App Store 下载链接,也不应该把它写成已经发布的产品。这个问题我会留到后面继续处理。先把账户与设备关联弄清楚,再谈正式签名、测试分发和上架,会比绕过注册问题更稳妥。

现在怎样在自己的设备上构建

项目当前最低支持 iOS 18,Apple Watch 目标最低支持 watchOS 11。构建前需要准备 Xcode 27 或更高版本,以及自己的 Apple Developer Team。

基本步骤如下:

  1. 用 Xcode 打开 ParcelBoard.xcodeproj
  2. 在主 App、小组件和 Apple Watch 三个 Target 的 Signing & Capabilities 中选择同一个 Team。
  3. 为自己的构建修改 Bundle Identifier,并保持 Watch Identifier 以 iPhone App Identifier 为前缀。
  4. 确认主 App 与 Widget 扩展启用了同名 App Group;如果改了组标识,代码和 entitlement 也要一起修改。
  5. 连接 iPhone,在 Xcode 顶部选择真机,按 Command + R

只想看界面时,可以先开启演示模式,不填任何快递服务账号。要查询真实物流,再到设置中选择快递100或自己的 HTTPS 代理。

当前局限

ParcelBoard 已经能在我的测试环境中使用,但离正式产品仍有距离。

  • 工程仍是测试项目,签名、App Group、Widget 和 Watch Target 对第一次构建的人并不算轻松。
  • Apple Developer Program 注册仍被受信任设备关联问题卡住,因此目前无法通过 TestFlight 或 App Store 分发。
  • 地图只是物流节点示意,不代表道路轨迹或车辆实时位置;缺少坐标时,地理编码也可能失败。
  • 本地后台刷新没有固定时刻,不能代替服务端推送。
  • 快递100直连适合个人测试,公开分发前必须把共享凭据移到服务端;用户量增加后还要为查询额度准备持续预算。
  • 实时活动受系统缓存、活动数量和载荷限制影响,系统升级后仍需要持续真机验证。
  • 扫描、剪贴板与取件码自动化都受 iOS 权限和隐私边界约束,不可能完全无感运行。
  • README 里的旧版本说明还没有完全跟上 3.1.12,工程设置与应用内更新日志才是当前版本依据。

此外,项目已经有 Kotlin、Compose 和 Material 3 的 Android 移植方向,也做了 Wear OS 与通知取件码的尝试。不过这篇只记录 iOS 主线,两边的系统能力和交互习惯不同,不适合简单照搬。

这段开发最真实的感受

ParcelBoard 从 1.0.03.1.12 只跨了一个多月,版本号看起来走得很快,实际大部分时间并不是在堆新功能。

我会把真机截图和现象交给 Codex,一起定位布局、状态和系统 API 的问题,再回到 Xcode 构建、安装、旋转屏幕、锁屏、点灵动岛、抬手看手表。很多修复第一次并不完整,列表点击连续发了两个紧急版本,地图详情也在同一套设计上改了许多次。

这种过程有点慢,却比只在模拟器里把界面做漂亮更可靠。一个包裹 App 真正难的地方,不是画出一张列表,而是让同一份状态在主 App、地图、通知、锁屏、小组件和手表之间保持一致,同时不越过系统边界。

接下来我还会继续开发 ParcelBoard。短期内先处理开发者账户与受信任设备的关联问题,整理当前工程和文档,继续修正真机细节,再把查询、推送和发布方式做得更适合长期使用。它现在还不是一个可以交付完就不再管的成品,但已经从最初那个本地列表,变成了我愿意每天打开的工具。