You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI嵌套navigationDestination致@StateObject多次初始化问题

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 23:14:53