Observable Object got retained in ViewController-making closure after VC deinited.
Closed this issue · 1 comments
Description
Consider ViewModel
is a @Observable class
.
If I capture the viewModel in the ViewController-making content closure of .present
(or any method else) like this:
@Observable
class ViewModel {
var isPresented = false
}
class SheetViewController: UIViewController {
@UIBindable var viewModel = ViewModel()
override func viewDidLoad() {
present(isPresented: $viewModel.isPresented) { [viewModel] in
_ = viewModel
return UIViewController()
}
}
}
After dismissing SheetViewController, the viewModel keeps alive. Even SheetViewController did actually been deinited.
I'm not sure this is a bug or expected behavior.
If I have to use ViewModel to create another UIViewController, how can I avoid this problem and get ViewModel be released?
Maybe something like this?
present(isPresented: $viewModel.isPresented) { [weak viewModel] in
guard let viewModel else { return UIViewController() }
return MyRealViewController(viewModel: viewModel)
}
Another similar situation
If I use observe
and capture ViewModel in the closure, the ViewModel will be retained after dismissing only when I access the property of ViewModel. This behavior is expected because the closure was "registered" on the ViewModel itself.
// viewModel will be retained after dismissing, which is expected.
observe { [viewModel] in
_ = viewModel.isThirdVCPresented
}
But in the example code of the main topic, I didn't even access any property of ViewModel.
Checklist
- I have determined whether this bug is also reproducible in a vanilla SwiftUI project.
- If possible, I've reproduced the issue using the
main
branch of this package. - This issue hasn't been addressed in an existing GitHub issue or discussion.
Expected behavior
ViewModel should be released.
Actual behavior
ViewModel got retained.
Steps to reproduce
I've uploaded a tiny project to reproduce this problem.
https://github.com/Lumisilk/SwiftNavigationBugExample
SwiftUI Navigation version information
2.2.2
Destination operating system
iOS 17, 18
Xcode version information
Xcode 16.1
Swift Compiler version information
swift-driver version: 1.115 Apple Swift version 6.0.2 (swiftlang-6.0.2.1.2 clang-1600.0.26.4)
Target: arm64-apple-macosx15.0
Hi @Lumisilk, in general it is best to only capture weak self
in these closures. You shouldn't need to capture things like viewModel
inside present(isPresented:)
because if you need that data in the trailing closure, then you should simply make it a part of the data that drives navigation using present(item:)
:
present(item: $model.child) { childModel in
ChildViewController(model: childModel)
}
Since I don't think this is an issue with the library I am going to convert it to a discussion. Please feel free to continue the conversation over there!