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

SwiftUI中导航与子视图的依赖管理通用原则:如何避免视图重绘时重复创建实例

SwiftUI中导航与子视图的依赖管理通用原则:如何避免视图重绘时重复创建实例

我完全懂你的这种纠结——SwiftUI的视图重绘机制和导航API的内部逻辑真的很容易让人在依赖管理上踩坑,尤其是当你想确保重量级实例(比如ViewModel、服务)只被创建一次的时候。你提到的navigationDestination闭包多次触发的问题,其实是SwiftUI视图系统的正常行为(用来预渲染或校验视图描述),所以我们不能依赖闭包只执行一次,得从SwiftUI的核心本质出发,建立一套通用的依赖管理原则。

首先,先明确SwiftUI视图的核心本质:

SwiftUI的视图结构体(比如你的ChatsListContainerView)是状态的描述,不是真正的UI实例。视图的body会被频繁调用(比如父视图状态变化、设备旋转、甚至系统内部更新),所以绝对不能在body或body调用的闭包(比如navigationDestination的内容闭包)中创建重量级依赖——每次body执行都会重新创建这些实例,这就是你遇到重复实例的直接原因。


通用原则1:把依赖创建移出视图的渲染流程

所有依赖(ViewModel、服务实例、子视图的装配逻辑)都应该在视图结构体之外创建,比如:

  • 专门的装配器(Assembly)类/枚举(就像你写的CommunityAssembly)
  • 父ViewModel内部
  • 全局环境(Environment)

你的CommunityAssembly是个很好的起点,但要注意:不要让navigationDestination的闭包每次调用都执行makeModule——因为闭包可能被多次触发,导致每次都新建ViewModel。我们需要把“创建逻辑”和“实例缓存”分开。


通用原则2:用「状态持有层」管理导航目标的依赖

不要让视图持有子视图或依赖的实例,而是让**父ViewModel(或专门的导航状态管理器)**持有已创建的依赖缓存,把依赖和导航数据绑定起来。

比如修改你的ChatsListViewModel,让它维护一个和CommunityInputData对应的ViewModel缓存:

// 先给ViewModel协议补充缓存相关的能力
protocol ChatsListViewModelProtocol: ObservableObject {
    var communityDetailsInputData: CommunityInputData? { get set }
    func prepareCommunityNavigation(for inputData: CommunityInputData)
    func getCommunityViewModel(for id: CommunityInputData.ID) -> CommunityViewModelProtocol?
}

class ChatsListViewModel: ChatsListViewModelProtocol {
    @Published var communityDetailsInputData: CommunityInputData?
    // 缓存:用输入数据的ID作为key,绑定对应的ViewModel
    private var communityViewModelCache: [CommunityInputData.ID: CommunityViewModelProtocol] = [:]
    // 注入装配器,让ViewModel不直接依赖具体的装配实现
    private let communityAssembly: CommunityAssemblyProtocol
    
    init(communityAssembly: CommunityAssemblyProtocol) {
        self.communityAssembly = communityAssembly
    }
    
    // 准备导航时,先检查缓存,没有再创建
    func prepareCommunityNavigation(for inputData: CommunityInputData) {
        if communityViewModelCache[inputData.id] == nil {
            let vm = communityAssembly.makeCommunityViewModel(inputData: inputData)
            communityViewModelCache[inputData.id] = vm
        }
        // 触发导航
        communityDetailsInputData = inputData
    }
    
    // 提供获取缓存ViewModel的方法
    func getCommunityViewModel(for id: CommunityInputData.ID) -> CommunityViewModelProtocol? {
        communityViewModelCache[id]
    }
}

然后在你的ChatsListContainerView中,navigationDestination的闭包只需要从父ViewModel的缓存中获取实例,而不是新建:

.navigationDestination(item: self.$viewModel.communityDetailsInputData) { inputData in
    guard let communityVM = viewModel.getCommunityViewModel(for: inputData.id) else {
        // 兜底逻辑:理论上不会走到这里,因为prepareCommunityNavigation已经创建了实例
        return communityViewBuilder(inputData)
    }
    // 直接用缓存好的ViewModel创建视图
    CommunityContainerView(viewModel: communityVM)
}

通用原则3:区分依赖的生命周期,选择合适的注入方式

根据依赖的生命周期,选择不同的注入方式:

  1. 全局服务(比如CommunityService、ChatsService):
    用Environment注入,或者通过装配器传递(避免直接用ServiceLocator.shared,不利于单元测试)。比如:

    // 先给服务定义协议,方便注入
    protocol CommunityServiceProtocol { /* ... */ }
    protocol ChatsServiceProtocol { /* ... */ }
    
    // 给环境定义键值
    private struct CommunityServiceKey: EnvironmentKey {
        static let defaultValue: CommunityServiceProtocol = ServiceLocator.shared.communityService
    }
    private struct ChatsServiceKey: EnvironmentKey {
        static let defaultValue: ChatsServiceProtocol = ServiceLocator.shared.chatsService
    }
    
    extension EnvironmentValues {
        var communityService: CommunityServiceProtocol {
            get { self[CommunityServiceKey.self] }
            set { self[CommunityServiceKey.self] = newValue }
        }
        var chatsService: ChatsServiceProtocol {
            get { self[ChatsServiceKey.self] }
            set { self[ChatsServiceKey.self] = newValue }
        }
    }
    
    // 在App入口注入环境
    @main
    struct MyApp: App {
        var body: some Scene {
            WindowGroup {
                ChatsListContainerView(
                    viewModel: ChatsListViewModel(communityAssembly: CommunityAssembly()),
                    communityViewBuilder: { data in
                        CommunityAssembly.makeModule(inputData: data)
                    }
                )
            }
        }
    }
    
    // 装配器从环境中获取服务
    enum CommunityAssembly {
        static func makeModule(inputData: CommunityInputData) -> some View {
            CommunityContainerView(
                viewModel: CommunityViewModel(
                    viewState: .loading,
                    communityService: .init(),
                    chatsService: .init(),
                    communityId: inputData.id
                )
            )
        }
    }
    
  2. ViewModel级依赖(比如CommunityViewModel):
    用装配器创建,由父ViewModel缓存,和导航数据的生命周期绑定——用户导航到同一个数据时,复用同一个ViewModel实例。


回答你最关心的几个具体问题

  1. 「视图应该存储子视图/导航视图的实例吗?还是只存储它们的依赖?」
    都不应该。视图的唯一职责是根据状态渲染UI,以及把用户动作转发给ViewModel。视图不应该持有任何子视图或依赖的实例——这些都应该由ViewModel或装配器来管理。视图只需要接收ViewModel(@ObservedObject/@StateObject)和必要的闭包(比如你的communityViewBuilder)。

  2. 「如何避免视图重绘时重复创建导航/子视图的外部依赖?」
    核心就是:

    • 把依赖创建移出视图的body和渲染闭包
    • 用父ViewModel或状态管理器缓存依赖实例,和对应的业务数据绑定
    • 永远不要依赖SwiftUI的内部行为(比如navigationDestination只触发一次)来保证实例唯一性

最后,总结一下干净的依赖管理流程:

  1. 用装配器统一管理所有ViewModel的创建逻辑,从环境或服务定位器获取底层服务
  2. 父ViewModel负责维护导航目标的依赖缓存,把依赖和业务数据绑定
  3. 视图只负责渲染状态和转发动作,不参与任何依赖的创建或持有
  4. 避免在body或其调用的闭包中创建重量级实例

这样就能彻底解决重复实例的问题,同时保持代码的解耦和可测试性。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.08 14:03:21