6.1 综合实战:阳台花园长成完整的树


6.1 综合实战:阳台花园长成完整的树

本节摘要:零件全齐,合拢成树。本节以工程视角重走一遍完整流程:开工前画树形图定结构、定数据流走向,然后自外向内装配(标签底座 → 导航主干 → 各页面),给仓库补上"重启不丢数据"的持久化雏形,最后跑一份覆盖所有分支路径的回归清单。没有任何新知识点,难的是顺序与纪律。

开工前:把树形画在纸上

前五章的零件散落在各节:模型(第 2 章)、UIKit 详情页(第 3 章)、列表与卡片(第 4 章)、SwiftUI 全家桶(第 5 章)。动手装配前,先在纸上(或注释里)把这棵树画出来——这不是仪式感,是防止"边写边长歪"的图纸:

图 6-1 阳台花园完整视图树:结构与数据流一图流

图 6-1 阳台花园完整视图树:结构与数据流一图流

图定三件事:页面清单(六个页面)、归属框架(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 重读已经够用,别过度设计。

本节要点回顾

  • 开工先画树:页面清单、框架归属、数据流向三样定在纸面再动手;
  • 装配自外向内:底座 → 仓库 → 页面,每层跑通再进下一层;
  • 仓库单一且注入:持久化抽象成协议,可换实现可测试;
  • 回归清单走全部分支,空态与校验是最常被漏的两项;
  • 双框架更新路径不同:SwiftUI 自动、UIKit 靠生命周期主动重读,验收项必须有跨框架那条。

树合拢了,但它会生病——下一节是随身的病虫害手册。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U