本节摘要:零件全齐,合拢成树。本节以工程视角重走一遍完整流程:开工前画树形图定结构、定数据流走向,然后自外向内装配(标签底座 → 导航主干 → 各页面),给仓库补上"重启不丢数据"的持久化雏形,最后跑一份覆盖所有分支路径的回归清单。没有任何新知识点,难的是顺序与纪律。
前五章的零件散落在各节:模型(第 2 章)、UIKit 详情页(第 3 章)、列表与卡片(第 4 章)、SwiftUI 全家桶(第 5 章)。动手装配前,先在纸上(或注释里)把这棵树画出来——这不是仪式感,是防止"边写边长歪"的图纸:

图定三件事:页面清单(六个页面)、归属框架(UIKit 与 SwiftUI 各管哪几页)、数据流(全部页面读写同一个仓库)。值得说明的是这个"双框架分工"是教学需要——真实项目往往全 UIKit 或全 SwiftUI,但正因为两套并存,6.3 的共存技术才有落地现场。
装配顺序自外向内,每层跑一次模拟器再进下一层——出问题时你永远知道是哪层引入的:
// 第一层:底座(场景装配处) func installRoot() { let gardenNav = UINavigationController(rootViewController: PlantListTableViewController()) gardenNav.tabBarItem = UITabBarItem(title: "花园", image: nil, tag: 0) // SwiftUI 页面装进 UIKit 世界的桥:HostingController(6.3 详讲) let favVC = UIHostingController(rootView: FavoriteScreen()) let favNav = UINavigationController(rootViewController: favVC) favNav.tabBarItem = UITabBarItem(title: "收藏", image: nil, tag: 1) let settingsVC = UIHostingController(rootView: SettingsScreen()) settingsVC.tabBarItem = UITabBarItem(title: "设置", image: nil, tag: 2) let tabs = UITabBarController() tabs.viewControllers = [gardenNav, favNav, settingsVC] window?.rootViewController = tabs print("底座装配完成:三条枝") } // 输出:底座装配完成:三条枝(随后各页 viewDidLoad 依次触发)
第二层:数据仓库与依赖注入。仓库是 2.5 的类加 5.3 的可观察外衣,注入方式选"构造传入"(比全局单例可测试):
final class PlantStore: ObservableObject { @Published private(set) var plants: [Plant] = [] private let storage: PlantStorage // 持久化协议(2.6):可换实现 init(storage: PlantStorage = FilePlantStorage()) { self.storage = storage plants = storage.load() // 启动时读档 print("仓库开张,读档 \(plants.count) 株") } func add(_ p: Plant) { plants.append(p); storage.save(plants) } func remove(_ p: Plant) { plants.removeAll { $0.id == p.id }; storage.save(plants) } func water(_ p: Plant) { if let i = plants.firstIndex(where: { $0.id == p.id }) { plants[i].water(); storage.save(plants) print("已浇水:\(p.name),档已存") // 已浇水:龟背竹,档已存 } } } protocol PlantStorage { // 持久化抽象(第 6 章雏形) func load() -> [Plant] func save(_ plants: [Plant]) } struct FilePlantStorage: PlantStorage { // 文件实现(真实项目换成数据库) private let url = FileManager.default .urls(for: .documentDirectory, in: .userDomainMask)[0] .appendingPathComponent("garden.json") func load() -> [Plant] { guard let data = try? Data(contentsOf: url) else { return [] } return (try? JSONDecoder().decode([Plant].self, from: data)) ?? [] } func save(_ plants: [Plant]) { if let data = try? JSONEncoder().encode(plants) { try? data.write(to: url) } } } // 效果:杀掉 App 重开,昨天存的植物与浇水记录都在(详情页 passedDays 原样)
第三层:页面内部。各页只做两件事——拿仓库引用(构造注入或环境注入)、把自己的界面接上(UIKit 走 3.1 的 render,SwiftUI 走 5.3 的状态观察)。这些代码前五章都写过,此处不重复,装配时照搬即可。
合拢之后最容易漏的是"路径没走全"。收工前按这份清单点一遍,每项都能复现即通过:
| 路径 | 操作 | 预期 |
|---|---|---|
| 空态 | 清空数据后进花园 | 显示空态引导(4.1),不显示空表格 |
| 添加 | 点 + 填名字存入 | 新行出现在列表,sheet 收起,重启仍在 |
| 校验 | 名字留空点保存 | 保存被拦(3.4 的 guard),表单不关 |
| 详情与浇水 | 点行进详情、点浇水 | 环变满、文案更新;返回列表该行状态同步 |
| 左滑 | 左滑行点已浇水 | 行刷新;详情侧再进也是新状态 |
| 删除 | 左滑删除 | 行消失且重启不复活 |
| 持久化 | 杀进程重开 | 数据与重启前一致 |
| 跨框架 | 收藏页(SwiftUI)浇水 | 花园页(UIKit)再进也是新状态——仓库广播生效 |
最后一行是双框架共存的关键验收:SwiftUI 的收藏页通过 ObservedObject 观察、UIKit 的花园页在 viewWillAppear 里重读仓库,两条更新路径殊途同归到同一份数据。若这项失败,说明有页面绕过仓库私存了数据——5.3 的纪律被破坏,回查。
背景:装配后花园页(UIKit 表格)怎么都不刷新——SwiftUI 收藏页浇水后,切回花园页行状态是旧的。操作:三步定位。先确认仓库唯一:搜索 new PlantStore,发现列表页自己又建了一个仓库(5.3 的经典违例);改为构造注入共享实例;再在 viewWillAppear 里加 renderAll 补上 UIKit 侧的主动刷新。结果:回归清单最后一行通过。解读:SwiftUI 侧"自动同步"掩盖了"两套机制并存"的事实——UIKit 页面没有差量更新,必须靠生命周期回调主动重读;这不是缺陷,是 5.1 对照表的镜像:命令式世界刷新是你的职责。变式:若 UIKit 页面也要实时响应,给仓库加通知或闭包回调让页面订阅——但多数场景 viewWillAppear 重读已经够用,别过度设计。
树合拢了,但它会生病——下一节是随身的病虫害手册。