本节摘要:单页长好了,多页要靠导航组织成园。UIKit 有三种栽法:UINavigationController 是主干(栈式推进,一层层往里钻)、UITabBarController 是侧枝(几个平行世界底部切换)、模态呈现是临时花(打断当前流程,办完就谢)。本节讲三者的结构语义与代码装配,演示阳台花园从单页长成"标签底座加导航主干"的三页结构,并给出"该用哪种"的判断准则。
到这里为止,阳台花园还只有一个详情页孤零零地立在窗口里。真实 App 的页面是成族的:列表、详情、添加、设置——它们之间的组织关系就是导航。UIKit 的三种容器各自对应一种用户心智:
三种不是互斥的:成熟 App 常是"标签底座 + 每个标签里各一条导航主干 + 关键节点上开临时花"。App 设置页在第三个标签、点某株植物进详情是第一条主干上 push、点添加按钮弹出的表单是模态——一园三种栽法并存。

结构图看懂了,代码从外往里装配。第一步,把三种容器组装起来作为窗口根:
// 场景配置处(AppDelegate 或 SceneDelegate)搭建整园结构 func installRoot() { // 三个标签各自的根:前两个是导航主干,第三个是普通页面 let listVC = PlantListViewController() // 植物列表(第 4 章实现) let gardenNav = UINavigationController(rootViewController: listVC) gardenNav.tabBarItem = UITabBarItem(title: "花园", image: nil, tag: 0) let favVC = FavoriteViewController() let favNav = UINavigationController(rootViewController: favVC) favNav.tabBarItem = UITabBarItem(title: "收藏", image: nil, tag: 1) let settingsVC = SettingsViewController() settingsVC.tabBarItem = UITabBarItem(title: "设置", image: nil, tag: 2) let tabBar = UITabBarController() tabBar.viewControllers = [gardenNav, favNav, settingsVC] // 底座装上三条枝 window?.rootViewController = tabBar print("结构:Tab → 三个孩子,前两个是 Nav") }
第二步,主干上的推进与返回。push 会自动带上返回按钮与滑动返回手势,全都免费:
// 列表页里:点某株植物 → push 详情 func openDetail(for plant: Plant) { let detail = PlantDetailViewController(plant: plant) navigationController?.pushViewController(detail, animated: true) // navigationController 可选链:如果本页不在任何导航主干里,静默不动(3.2 的链式思想) } // 详情页里:一层层退的三个档位 @objc func backTapped() { if let nav = navigationController { nav.popViewController(animated: true) // 退一层 // nav.popToRootViewController(animated: true) // 一路退到栈底 // dismiss(animated: true) // 这是模态的退法,别混用 } } // 每次进出栈,3.3 的生命周期回调成对触发:push 详情时列表走 WillDisappear/DidDisappear
第三步,模态。给列表页加"添加植物"按钮,点击升起表单:
@objc func addTapped() { let form = AddPlantViewController() // 3.4 的表单页 let nav = UINavigationController(rootViewController: form)// 模态通常包一层导航,好放标题与取消 nav.modalPresentationStyle = .pageSheet // 页面单式:底下留缝,暗示"还有下层" present(nav, animated: true) } // 表单内部结束自己: @objc func cancelTapped() { dismiss(animated: true) } // 落下 @objc func saveTapped() { // 先存数据…… dismiss(animated: true) }
⚠️ 常见坑:push 与 present 混淆——push 要本页在导航主干上(navigationController 非 nil),present 任何页面都能用;对应地,退场用 pop 还是 dismiss 必须与进入方式配对,用错轻则没反应,重则把别人的页面关了。进来的路就是出去的路。
背景:产品接连提了四个需求,每条都要判断栽法。操作与判定:
| 需求 | 判定 | 理由 |
|---|---|---|
| 点植物看详情与养护记录 | 主干 push | 层层深入,看完返回继续逛 |
| 顶部加"提醒设置"入口 | 标签侧枝 | 独立分区,与浏览流程无关 |
| 删除植物前弹确认 | 模态(警告式) | 打断流程逼用户决策,办完即散 |
| 从详情"编辑植物信息" | 主干 push | 编辑是详情的延伸层次,保留返回路径 |
解读:判定词只有三个问题——这是更深的层次吗(push)、这是平行的分区吗(tab)、这件事必须现在办完吗(present)。三个都答否,大概率该做成页内展开的区块而不是新页面。变式:删除确认这种轻量打断也可用行动作表(下拉的选项面板)替代整页模态——打断成本更低;苹果的人机界面指南对"破坏性操作"建议优先用行动作表加红色确认项,整页模态留给需要输入的流程。
至此第 3 章收工:图纸、根系、节律、叶片、伸展、主干俱全。下一章让树繁茂起来——列表、网格、手势、动画。