照顾宝宝通常是一家人的事。家庭同步让一位用户创建家庭并邀请家人加入, 随后家庭成员之间共享「照护对象」(宝宝 / 孕妈 / 宝妈)及其全部记录,包括辅食图片。
当前仅 iOS 端支持。Android / Web 暂未接入。
入口
首页右上角「…」菜单 → 家庭管理(home.menu.more.family)。
打开 FamilyManagementView,可查看家庭名称、当前角色(创建者 / 成员)、
家庭成员(照护对象)、参与者列表,并发起邀请。
技术架构
同步基于 CloudKit + CKShare,持久层使用 NSPersistentCloudKitContainer(Core Data)。
- 双 store:
PersistenceController同时挂载- 私有库(
.private)— 用户自己的数据; - 共享库(
.shared)— 通过 CKShare 接收的其他成员家庭数据。
- 私有库(
- 共享根对象:
CDFamily。container.share([family], to:)生成CKShare, 由UICloudSharingController(CloudSharingControllerView)呈现系统分享面板。 - 接受邀请:
SceneDelegate.windowScene(_:userDidAcceptCloudKitShareWith:)调用PersistenceController.acceptShare(metadata:),并发出.familyShareAccepted通知。 - 照护对象:
CDCareSubject(宝宝 / 孕妈 / 宝妈),以focusKey(baby:<UUID>/pregnancy/newMom)与现有筛选逻辑对应。
加性同步镜像(关键设计)
10 个本地 Store 仍以各自的 JSON 文件作为 UI 数据源(回归风险最低);
CloudSyncService 在其之上做一层镜像同步:
- 导出:把本地记录编码为通用信封
SyncEnvelope,写入CDRecord(kind+focusKey+ JSONpayload+updatedAt+deletedAt墓碑),挂到CDFamily。 - 导入:把共享库中的
CDRecord合并回各 Store,采用 后写入者优先(LWW,按updatedAt) + 墓碑删除。 - 防回环:导入期间设
isImporting屏蔽变更回调;导出按 0.6s 去抖。 - 同步路径在未登录 iCloud 时完全惰性,不影响本地功能。
合并逻辑集中在 SyncCodec(envelopes / merge),并有单元测试覆盖
(新增 / LWW / 墓碑 / 排序)。
图片共享
辅食图片原本只存相册 localIdentifier(仅本机有效)。为跨设备共享,
CloudPhotoExporter 读取图片字节写入 CDRecordPhoto(二进制 + 外部存储,
大文件由容器自动转 CKAsset);接收端缓存在 SolidFoodPhotoCache,
作为相册标识符无法解析时的回退数据源。
加入家庭:并入 / 保留私有
接受邀请后(.familyShareAccepted),RootView 弹出二选一:
- 并入家庭 — 显示家庭成员的共享记录(
includeSharedRecords = true); - 暂不合并 — 仅显示本地记录。
该选择由 CloudSyncService.includeSharedRecords(UserDefaults 持久化)控制,
可随时在「家庭管理 → 显示家庭共享记录」开关中更改。
独立 Apple 账号 vs 同一 Apple 家庭组
两者都走同一套 CKShare 机制,无需写两套逻辑,区别仅在加入时是否需要手动接受:
- 独立 Apple 账号:被邀请者需在邀请(链接 / 系统面板)中点击接受。
- 同一 Apple 家庭组:成员可由系统自动接受(具体行为随 iOS 版本略有差异)。
没有公开 API 可枚举 Apple 家庭组成员名单;分享数据归创建者所有、占用创建者 的 iCloud 空间,被邀请者不额外付费。
配置
- 工程由 XcodeGen 从
project.yml生成 —— iCloud 能力、entitlements、 Background Modes 均写在project.yml,改后须pnpm ios:gen,不要手改.xcodeproj。 - 容器标识:
iCloud.com.gluttongk.apps.illyasviel。 - entitlements:
apps/ios/BabyPlan/BabyPlan.entitlements(CloudKit 服务 + aps-environment)。
真机测试清单
CloudKit / CKShare 无法在模拟器或无签名环境验证,需真机实测:
- 两台真机分别登录不同 Apple 账号,均开启 iCloud。
- 设备 A:家庭管理 → 邀请家人加入 → 通过系统面板发送邀请。
- 设备 B:打开邀请 → 接受 → 选择「并入家庭」。
- 设备 A 新增一条记录(含辅食图片),确认设备 B 能看到记录与图片。
- 设备 B 修改 / 删除记录,确认设备 A 同步更新(LWW / 墓碑)。
- 如两台设备属于同一 Apple 家庭组,验证自动接受行为。
本地可验证部分
同步核心逻辑由 BabyPlanTests(host-less 逻辑测试包)覆盖:SyncCodec
合并 / LWW / 墓碑 / 排序,以及 CDRecord / CDRecordPhoto 的 Core Data 往返
(含图片二进制)。运行:
cd apps/ios && xcodegen generate && \
xcodebuild -project BabyPlan.xcodeproj -scheme BabyPlan \
-sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 17 Pro' \
test