SwiftUI嵌套navigationDestination致@StateObject多次初始化问题
问题描述
基于NavigationStack和.navigationDestination()构建嵌套导航层级:
- ContentView - ViewA.navigationDestination() - ViewB.navigationDestination() - ...
每个视图通过自身的.navigationDestination()处理导航逻辑,向顶层NavigationPath添加元素触发页面跳转。点击ViewA的"Down to B"按钮后,ViewB被推入导航栈,但绑定的@StateObject var storeB = StoreB()会出现先初始化、再销毁、最后重新初始化的现象,控制台输出如下:
+++ Store B inited --- Store B deinited +++ Store B inited
若将ViewB内部的.navigationDestination()移至ViewA的导航目标代码块中,重复初始化问题会消失,但希望保持导航逻辑与对应视图绑定的代码结构。
原因分析
这并非SwiftUI Bug,而是嵌套navigationDestination的正常行为:
- 当
NavigationStack检测到NavigationPath变化时,会遍历当前视图树中的所有.navigationDestination修饰符,匹配对应的路由规则。 - 此过程中SwiftUI会临时创建ViewB的实例,目的是解析它内部定义的
.navigationDestination(for: RouteToC.self)规则,这个临时实例的@StateObject会被初始化,但因为仅用于解析导航规则,实例很快会被销毁。 - 当路由匹配完成后,SwiftUI才会创建真正进入导航栈的ViewB实例,此时
@StateObject会再次初始化并被长期保留。
解决方案
方案1:用LazyView延迟视图创建
通过自定义LazyView包装ViewB,避免临时解析阶段创建完整的ViewB实例(包括@StateObject):
首先定义LazyView:
struct LazyView<Content: View>: View { let build: () -> Content init(_ build: @autoclosure @escaping () -> Content) { self.build = build } var body: some View { build() } }
修改ViewA中的导航目标代码:
.navigationDestination(for: RouteToB.self) { route in LazyView(ViewB(path: $path)) // 用LazyView包裹ViewB }
只有当ViewB真正需要被推入导航栈时,才会执行ViewB(path: $path)的初始化,临时解析阶段只会创建轻量的LazyView实例,不会触发ViewB的@StateObject初始化。
方案2:惰性注入StateObject(iOS14+)
将StoreB的初始化改为外部注入,通过@StateObject的初始化器延迟创建对象:
修改ViewB的定义:
struct ViewB: View { @Binding var path: NavigationPath @StateObject private var storeB: StoreB // 提供外部初始化器,默认创建StoreB init(path: Binding<NavigationPath>, storeB: StoreB = StoreB()) { self._path = path self._storeB = StateObject(wrappedValue: storeB) } // 其余代码保持不变 }
此方式下,临时创建的ViewB实例虽会初始化StoreB,但@StateObject的生命周期管理特性会确保真正进入栈的实例保留正确的对象实例。
方案3:使用iOS17+的@Observable宏
若目标系统为iOS17及以上,可使用新的@Observable宏替代ObservableObject,它的生命周期管理更高效,能避免此类重复初始化问题:
@Observable class StoreB { private let value = 8 init() { print("+++ Store B inited") } deinit { print("--- Store B deinited") } }
然后在ViewB中替换@StateObject为@State:
@State private var storeB = StoreB()
@Observable配合@State的组合在处理视图临时创建时,会更智能地管理对象生命周期,避免不必要的初始化操作。
验证代码(方案1)
修改后的完整代码示例:
struct ContentView: View { @State var path = NavigationPath() var body: some View { NavigationStack(path: $path) { ViewA(path: $path) } } } // MARK: - View A struct ViewA: View { @Binding var path: NavigationPath var body: some View { Button("Down to B") { path.append(RouteToB()) } .navigationDestination(for: RouteToB.self) { route in LazyView(ViewB(path: $path)) } } struct RouteToB: Hashable { } } // MARK: - View B struct ViewB: View { @Binding var path: NavigationPath @StateObject var storeB = StoreB() var body: some View { Button("Down to C") { path.append(RouteToC()) } .navigationDestination(for: RouteToC.self) { route in Color.green } } struct RouteToC: Hashable { } class StoreB: ObservableObject { private let value = 8 init() { print("+++ Store B inited") } deinit { print("--- Store B deinited") } } } // 自定义LazyView struct LazyView<Content: View>: View { let build: () -> Content init(_ build: @autoclosure @escaping () -> Content) { self.build = build } var body: some View { build() } }
此时点击"Down to B",控制台只会输出一次+++ Store B inited,无销毁日志,符合预期。
内容的提问来源于stack exchange,提问作者FunnyHorse

