pointfreeco/swift-navigation

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!