NavigationView环境下ViewModel无法自动释放的问题及解决方法
问题根源
这个问题本质是NavigationView的内部视图栈会持有EnvironmentObject的强引用,再加上如果ViewModel和视图、闭包之间存在未处理的循环引用,就会导致ViewModel在宿主视图(LoggedInView)销毁后无法被释放,deinit方法始终不执行。
解决方案
下面分两种常见场景给出具体的代码调整方案:
场景1:ViewModel由LoggedInView创建
这种情况推荐用@StateObject管理ViewModel的生命周期,并调整NavigationView的层级,让它成为LoggedInView的子视图:
// 父视图(比如ContentView) struct ContentView: View { @State private var isLoggedIn = false var body: some View { if isLoggedIn { LoggedInView(onLogout: { isLoggedIn = false }) } else { LoginView(onLogin: { isLoggedIn = true }) } } } // 登录后视图 struct LoggedInView: View { let onLogout: () -> Void @StateObject private var viewModel = LoggedInViewModel() // 由SwiftUI管理生命周期 var body: some View { NavigationView { VStack { Text("已登录") Button("退出登录") { onLogout() } } } .environmentObject(viewModel) // 将ViewModel注入NavigationView内部环境 } } // ViewModel class LoggedInViewModel: ObservableObject { deinit { print("ViewModel 已释放") } // 若有异步操作/闭包,必须用weak self避免循环引用 func fetchData() { DispatchQueue.global().async { [weak self] in guard let self = self else { return } // 执行数据请求操作 } } }
为什么这样有效?
@StateObject会严格绑定宿主视图(LoggedInView)的生命周期,当LoggedInView被销毁时,ViewModel会被自动释放。- NavigationView作为LoggedInView的子视图,当LoggedInView消失时,NavigationView也会被销毁,不会残留对ViewModel的引用。
场景2:ViewModel由父视图注入
如果必须在父视图中创建ViewModel并注入环境,需要确保父视图在切换状态时主动销毁ViewModel:
struct ContentView: View { @State private var loggedInVM: LoggedInViewModel? // 用可选类型管理 var body: some View { NavigationView { if let viewModel = loggedInVM { LoggedInView() .environmentObject(viewModel) .navigationBarItems(trailing: Button("退出") { loggedInVM = nil // 主动置空销毁ViewModel }) } else { LoginView(onLogin: { loggedInVM = LoggedInViewModel() }) } } } }
这种方式下,当点击退出时将loggedInVM置为nil,ViewModel失去所有强引用后会被系统释放。
关键注意点
- 永远检查ViewModel内部的闭包、回调,必须用
[weak self]避免强引用循环,这是最容易被忽略的点。 - 尽量避免让NavigationView作为持有环境对象的父容器,否则容易出现视图栈残留引用的问题。
内容的提问来源于stack exchange,提问作者Jack Vanderpump
相关产品推荐
相关产品推荐

