Core Data与CollectionView UI实时同步:CompositionalDiffablePlayground Jokes示例收藏、上下文菜单与骨架屏动画完整实现
【免费下载链接】CompositionalDiffablePlayground.iosExamples showing Compositional Layout, Diffable Data Sources and more!项目地址: https://gitcode.com/gh_mirrors/co/CompositionalDiffablePlayground.ios
CompositionalDiffablePlayground.ios 是一个专门演练 iOS 现代 CollectionView API 的示例工程,核心演示 Compositional Layout(组合式布局)与 Diffable Data Source(可差分数据源)。本文以其中功能最完整的Jokes(笑话列表)示例为主线,拆解Core Data 与 CollectionView UI 实时同步的完整机制,涵盖收藏持久化、长按上下文菜单(朗读/取消收藏)与骨架屏闪烁动画的实现思路,帮助新手理解 Diffable、Compositional 与 Core Data 三者在真实项目中如何协作。
项目概览:现代 CollectionView API 的练习场 🎯
主界面 ViewController.swift 本身也是一个由 Diffable 驱动的 CollectionView,菜单分为两区:
- Layout 基础示例:列表、简单网格、懒加载网格、吸顶头部、响应式布局、Onboarding 流程等 8 种纯布局玩法;
- 复杂实战示例:Jokes 笑话列表、Apple Photos 风格画廊(见下方项目自带示例图片)、Instagram 风格个人主页、Indie App 新闻流等。
其中 Jokes 示例是唯一同时串联了网络加载 + Core Data 收藏 + 上下文菜单 + 语音朗读 + 骨架屏的完整场景,因此值得单独拆解。
Core Data 与 CollectionView UI 如何保持实时同步
整个同步链路只由四个角色完成,核心思想是:UI 永远只由 snapshot 驱动,而 snapshot 又永远只从 Core Data 和网络的最新结果构建。
| 角色 | 文件 | 职责 |
|---|---|---|
| 数据库单例 | Database.swift | 持有NSPersistentContainer,暴露viewContext,封装saveContext() |
| 数据模型 | Joke.swift | Joke: NSManagedObject,字段为 id、setup、punchline、created |
| 实体定义 | contents | 可视化数据模型中的Joke实体 |
| 同步桥 | JokesViewController.swift | 用NSFetchedResultsController监听数据库变化 |
关键在 JokesViewController.swift 末尾这个只有两行的 delegate:
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) { datasource.apply(self.snapshot(), animatingDifferences: true) }只要 Core Data 中Joke记录发生增、删、改,NSFetchedResultsController就会回调它;而apply(_:animatingDifferences:)会自动对比新旧快照,只重绘变化的行,并附带原生插入/移除动画。收藏或删除一条记录,UI 在同一个 runloop 内自动刷新——这就是“实时同步”的全部秘密。
一键收藏:快照驱动的 UI 更新 ⭐
收藏入口在 JokesViewController.swift 的两个方法中:
favoriteJoke(_:):先用Joke.byIdFetchRequest(id:)查询是否已存在,防止重复收藏,再新建Joke对象写入并saveContext();unfavoriteJoke(withId:):按 id 取出记录后context.delete(_:),同样触发保存。
而snapshot()方法每次都直接从fetchedResultsController.fetchedObjects读取最新的收藏列表,构建出favoriteJokes(count:state:)分节——节标题显示Favorites (n),并可通过ActionButtonHeader按钮整体展开/折叠。
两个细节值得注意:
Joke.Diffable的hash(into:)只合并id(见 Joke.swift),保证 Diffable 算法能正确识别“同一条笑话”,内容变化不会误判为增删;- JokeCell.swift 根据数据来源切换星形图标:来自网络的显示空心星(可收藏),来自 Core Data 的显示实心星(已收藏),一眼区分两种状态。
长按上下文菜单:把笑话读出来 🎙️
长按菜单由 JokesViewController.swift 的contextMenuConfigurationForItemAt实现,逻辑简洁:
- 先通过
datasource.itemIdentifier(for:)反查当前 item,骨架屏占位项(.loading)直接返回 nil 不显示菜单; - 固定提供Hear动作:用
AVSpeechSynthesizer朗读setup + punchline; - 仅当 item 属于收藏分节时,额外追加Unfavorite动作,调用前面提到的删除逻辑。
由于删除会走 Core Data 保存 → FRC 回调 →apply(snapshot)的全链路,菜单里的“取消收藏”同样享受自动 diff 动画,无需手写任何刷新代码。

骨架屏闪烁动画:20 行代码做出 shimmer 效果 ⚡
数据未返回时,snapshot()会向.jokes分节填入 8 个Item.loading(UUID)占位项(每个 UUID 独立,避免复用同一身份),cell 分支中调用showLoading()(见 JokeCell.swift):
- 创建一个
CAGradientLayer,颜色为透明 → 10% 黑色 → 透明,宽度放大 3 倍并左移,作为“光带”; - 用
CABasicAnimation动画locations属性从[0.0, 0.1, 0.2]到[0.8, 0.9, 1.0],duration = 1.1、repeatCount = .infinity,得到循环扫过的 shimmer 效果; - 在
prepareForReuse()中执行shimmerLayer?.removeFromSuperlayer(),防止 cell 复用时残留动画。
这套“占位 item + 渐变光带动画 + 复用时清理”的组合,可以直接迁移到任何 Diffable 列表。
关键文件路径速查表
| 功能模块 | 文件路径 |
|---|---|
| 笑话列表控制器(快照/FRC 同步/上下文菜单) | ViewControllers/JokesViewController.swift |
| 骨架屏 shimmer 动画单元格 | Views/JokeCell.swift |
| 网络笑话 DTO | Model/DTO/JokeDTO.swift |
| 共享协议(setup/punchline) | Protocols/JokeProtocol.swift |
| Core Data 实体与排序请求 | Database/Joke.swift |
| 持久层单例 | Database/Database.swift |
| 实体模型文件 | Database.xcdatamodel/contents |
| 网络请求(official-joke-api) | Networking/SimpleNetworkHelper.swift |
| 通用排序请求扩展 | Database/Managed.swift |
获取项目并运行 🚀
执行以下命令克隆仓库,用 Xcode 打开工程,选择 iOS 13+ 模拟器运行即可(Jokes 需要网络访问笑话 API):
git clone https://gitcode.com/gh_mirrors/co/CompositionalDiffablePlayground.ios运行后在主页第二个分节点击Jokes list即可体验完整效果:进入时先看到 8 格 shimmer 骨架屏,加载完成后逐行动画填充;点星形收藏会实时出现在顶部 Favorites 分节;长按任意一条笑话可朗读或取消收藏。对照文中四个核心文件阅读源码,是掌握Core Data 与 CollectionView 实时同步的最佳路径。
【免费下载链接】CompositionalDiffablePlayground.iosExamples showing Compositional Layout, Diffable Data Sources and more!项目地址: https://gitcode.com/gh_mirrors/co/CompositionalDiffablePlayground.ios
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考