SwiftUI 导航的三个阶段:从 NavigationStack 到深层链接
SwiftUI 导航的三个阶段:从 NavigationStack 到深层链接
年前接了一个需求:App 从外部 URL 打开文章详情页,需要直接跳到对应页面而不是先展示首页。
听起来简单,做起来踩了一圈坑。SwiftUI 的导航系统在这三年里变了好几次,NavigationLink、NavigationController、NavigationStack 混在一起,文档和博客说法不一。今天把完整路径讲清楚。
第一阶段:NavigationLink 的简单时代
最早的 SwiftUI 导航只有 NavigationLink:
NavigationView {
List(items) { item in
NavigationLink(item.title) {
DetailView(item: item)
}
}
.navigationTitle("列表")
}
这种方式的问题:
NavigationView在 iPad 和 iPhone 上的布局行为不一样- 深层嵌套时,navigation bar 的状态管理混乱
- 不支持 programmatic navigation(代码驱动跳转)
第二阶段:NavigationStack 的现代化
iOS 16 引入 NavigationStack,解决了大部分历史问题:
NavigationStack {
List(items) { item in
NavigationLink(value: item) {
Text(item.title)
}
}
.navigationDestination(for: Item.self) { item in
DetailView(item: item)
}
}
关键变化是数据和导航路径分离。不再用 destination view 本身作为导航目标,而是用 value。
Path 的力量
NavigationPath 是这代 API 的核心:
struct RootView: View {
@State private var navigationPath = NavigationPath()
var body: some View {
NavigationStack(path: $navigationPath) {
List(items) { item in
NavigationLink(value: item) {
Text(item.title)
}
}
.navigationDestination(for: Item.self) { item in
DetailView(item: item)
}
}
}
}
你可以在任何地方往 navigationPath 里追加值来触发跳转:
func navigateToDetail(_ item: Item) {
navigationPath.append(item)
}
func navigateToSettings() {
navigationPath.append(NavigationDestination.settings)
}
第三阶段:深层链接实战
回到开头的问题:外部 URL 打开 App,需要直接跳到文章详情。
URL Scheme 处理
首先在 AppDelegate 或 SceneDelegate 里接收 URL:
@main
struct MyApp: App {
@StateObject private var appState = AppState()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(appState)
}
}
}
class AppDelegate: NSObject, UIApplicationDelegate {
func application(
_ app: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey: Any] = [:]
) -> Bool {
handleDeepLink(url)
return true
}
}
解析 URL 并同步到导航状态
关键在于:解析出目标页面后,如何驱动 NavigationStack 跳转?
@MainActor
final class AppState: ObservableObject {
@Published var navigationPath = NavigationPath()
@Published var pendingDeepLink: DeepLink?
func handleDeepLink(_ url: URL) {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
let host = components.host else { return }
switch host {
case "article":
if let id = components.queryItems?.first(where: { $0.name == "id" })?.value {
navigateToArticle(id: id)
}
case "profile":
if let userId = components.queryItems?.first(where: { $0.name == "userId" })?.value {
navigateToProfile(userId: userId)
}
default:
break
}
}
func navigateToArticle(id: String) {
// 先查数据,再跳转
Task {
if let article = await fetchArticle(id: id) {
navigationPath.append(article)
}
}
}
func navigateToProfile(userId: String) {
navigationPath.append(ProfileDestination(userId: userId))
}
}
完整的深层链接场景
假设 URL 是 myapp://article?id=12345
// 1. App 启动时恢复深层链接
struct MyApp: App {
@StateObject private var appState = AppState()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(appState)
.onOpenURL { url in
appState.handleDeepLink(url)
}
}
}
}
// 2. 根视图读取 pendingDeepLink 并执行跳转
struct ContentView: View {
@EnvironmentObject var appState: AppState
var body: some View {
TabView {
HomeTab()
ProfileTab()
}
}
}
struct HomeTab: View {
@EnvironmentObject var appState: AppState
var body: some View {
NavigationStack(path: $appState.navigationPath) {
ArticleListView()
.navigationDestination(for: Article.self) { article in
ArticleDetailView(article: article)
}
}
}
}
导航状态与 Tab 切换
这里有个容易踩的坑:TabView 里的 NavigationStack 如何管理各自的导航栈?
struct ContentView: View {
@EnvironmentObject var appState: AppState
var body: some View {
TabView {
HomeTab()
.tabItem { Label("首页", systemImage: "house") }
SearchTab()
.tabItem { Label("搜索", systemImage: "magnifyingglass") }
}
}
}
每个 Tab 有自己独立的 NavigationStack,切换 Tab 时 navigationPath 不会自动重置。如果你需要切换 Tab 时清空导航栈:
struct HomeTab: View {
@EnvironmentObject var appState: AppState
@State private var previousTab: Tab = .home
var body: some View {
NavigationStack(path: $appState.navigationPath) {
// ...
}
.onChange(of: appState.selectedTab) { oldTab, newTab in
if newTab != .home && oldTab == .home {
// 离开首页时重置导航栈
appState.navigationPath = NavigationPath()
}
previousTab = newTab
}
}
}
导航栏的精细控制
有时候详情页需要隐藏导航栏,但子页面又要显示:
struct ArticleDetailView: View {
let article: Article
@Environment(\.navigationPath) private var navigationPath
var body: some View {
ScrollView {
ArticleContent(article: article)
}
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .principal) {
Text(article.title)
.font(.headline)
}
}
}
}
struct ArticleContent: View {
let article: Article
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text(article.title)
.font(.title)
Text(article.body)
}
.padding()
}
}
总结
SwiftUI 导航进化的核心思路是声明式替代命令式:
- 用
NavigationStack+navigationDestination替代NavigationLink的嵌套写法 - 用
NavigationPath管理完整的导航栈,支持 programmatic navigation - 深层链接通过
onOpenURL接收 URL,解析后写入NavigationPath - Tab 间的导航栈隔离需要手动管理
理解了这个模型,就能优雅地处理各种导航场景,而不需要 hack 式的 DispatchQueue.main.async 或强行 popToRootViewController。
