架构
导航是一条独立的流程
页面不要自己 new 下一个业务页面。路由集中之后,深链和按钮走同一条路。
ViewController 里写 navigationController?.pushViewController(DetailViewController(id: id), animated: true),功能在小的时候很直接。等详情页还要在搜索、通知、小组件三条路进来,每一处都会复制一遍构造和入栈。漏掉的那条路参数就不一致。
路由模型
把“去哪里”写成数据,而不是写成对某个控制器的调用。路由器是唯一知道控制器类型的地方。页面只发出路由。
enum Route: Equatable {
case feed
case item(id: String)
case settings
}
protocol Routing: AnyObject {
func show(_ route: Route)
}
final class AppRouter: Routing {
private let navigation: UINavigationController
init(navigation: UINavigationController) { self.navigation = navigation }
func show(_ route: Route) {
switch route {
case .feed:
navigation.setViewControllers([FeedViewController(router: self)], animated: false)
case .item(let id):
navigation.pushViewController(ItemViewController(id: id, router: self), animated: true)
case .settings:
navigation.pushViewController(SettingsViewController(router: self), animated: true)
}
}
}
深链解析成 Route,和按钮调用的是同一个 show。参数校验放在解析处:id 为空就不要造出一个空的详情页。
不要把导航塞进通用状态机
FeedState 应该知道列表加载完了,不应该知道 UINavigationController。如果状态类型开始 import UIKit 来做 push,它就没法在没有界面的测试里跑,也会把这个功能焊死在 UIKit 上。
Coordinator 或 Router 可以按功能拆:Feed 流程管列表和详情,账号流程管登录。它们之间通过路由或委托交接,而不是拿到对方的控制器去改栈。一个流程结束,用完成回调把结果交回去,调用方决定下一程。
SwiftUI 里同样的划分是 NavigationPath 或一组枚举,由外层导航持有。子视图通过回调或环境中的路由器发路由,不自己拿 path.append 一个具体的内部页面类型,否则模块边界又破了。