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:区分依赖的生命周期,选择合适的注入方式
根据依赖的生命周期,选择不同的注入方式:
全局服务(比如
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 ) ) } }ViewModel级依赖(比如
CommunityViewModel):
用装配器创建,由父ViewModel缓存,和导航数据的生命周期绑定——用户导航到同一个数据时,复用同一个ViewModel实例。
回答你最关心的几个具体问题
「视图应该存储子视图/导航视图的实例吗?还是只存储它们的依赖?」
都不应该。视图的唯一职责是根据状态渲染UI,以及把用户动作转发给ViewModel。视图不应该持有任何子视图或依赖的实例——这些都应该由ViewModel或装配器来管理。视图只需要接收ViewModel(@ObservedObject/@StateObject)和必要的闭包(比如你的communityViewBuilder)。「如何避免视图重绘时重复创建导航/子视图的外部依赖?」
核心就是:- 把依赖创建移出视图的
body和渲染闭包 - 用父ViewModel或状态管理器缓存依赖实例,和对应的业务数据绑定
- 永远不要依赖SwiftUI的内部行为(比如
navigationDestination只触发一次)来保证实例唯一性
- 把依赖创建移出视图的
最后,总结一下干净的依赖管理流程:
- 用装配器统一管理所有ViewModel的创建逻辑,从环境或服务定位器获取底层服务
- 父ViewModel负责维护导航目标的依赖缓存,把依赖和业务数据绑定
- 视图只负责渲染状态和转发动作,不参与任何依赖的创建或持有
- 避免在
body或其调用的闭包中创建重量级实例
这样就能彻底解决重复实例的问题,同时保持代码的解耦和可测试性。
内容来源于stack exchange

