SwiftUI retain cycle with .searchable

I'm trying to see what I am doing wrong ... So I created this simple app where the stateobject Retainer won't get deallocated when I pop the view off the stack:

import SwiftUI

struct ContentView: View {
    
    var body: some View {
        NavigationView {
            List {
                NavigationLink("To Retain Cycle") {
                    RetainCycleView()
                }
            }
            .navigationTitle("Retain Cycle Demo")
        }
        .navigationViewStyle(.stack)
    }
}
    
struct RetainCycleView: View {
    
    @StateObject var model = Retainer()
//    @State var enteredText: String = ""
    
    var body: some View {
        VStack(alignment: .leading, spacing: 4) {
            Text("Navigate back to the previous view.")
            Text("You will see that 'Retainer' was NOT deallocated.")
            Text("(it's deinit function prints deallocing Retainer)")
                .font(.callout)
        }
        .padding()
        .searchable(text: $model.enteredText)
//       ^---- retain cycle
//        .searchable(text: $enteredText)
//       ^---- no retain cycle when using the @State var
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

class Retainer: ObservableObject {
    
    @Published var enteredText: String = ""
    
    init() { print("instantiated Retainer") }
    deinit { print("deallocing Retainer") }
}

I filed feedback but I am not entirely sure that this isn't me making some mistake ...

Please help me

If you do this then there is no need to create a new object model on each push. It is created once and destroyed once on app exit:

@main
struct MyApp: App {
    @StateObject var model = Retainer()
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(model)
        }
    }
}

struct RetainCycleView: View {
    @EnvironmentObject var model: Retainer
    var body: some View {
        VStack(alignment: .leading, spacing: 4) {
            Text("Navigate back to the previous view.")
            Text("You will see that 'Retainer' was NOT deallocated.")
            Text("(it's deinit function prints deallocing Retainer)")
                .font(.callout)
        }
        .padding()
        .searchable(text: $model.enteredText)
        .onChange(of: model.enteredText) { newValue in
            print("Searching for: ", newValue)
        }
    }
}

Yes that makes sense. Now the search model is created in the ancestor view ... I think that would be less ideal. The model is very light weight and essentially only providing a bindable publisher and some (web) search functionality ... this probably doesn't need to outlive that view and it would be ok to create a new model alongside a view push ...

Interestingly a TextField with the same binding to Retainer would behave as expected ... the model would be deallocated when the view is removed from the view stack ...

This still happens today on Xcode 26.4.1 / iOS 26.4.

Working around it by weakly capturing the view model in the binding:

.searchable(
  text: Binding(get: { [weak viewModel] in
      viewModel?.searchText ?? ""
    }, set: { [weak viewModel] in
      viewModel?.searchText = $0
    }),
  isPresented: $isSearchPresented,
  placement: .toolbar,
  prompt: Text(localizations.searchPrompt)
)

@hydrudcatt3

Thanks for the post. The thread is 4 years old, I think will be a great idea if you could post a new thread with all the details of the issue and the code you have posted.

Is what you experiencing like SwiftUI's implementation of .searchable sometimes holds onto the Binding longer than the view's lifecycle? Is that what you are seeing?

Looking forward to see your new post with all the details.

Albert
  Worldwide Developer Relations.

SwiftUI retain cycle with .searchable
 
 
Q