Memory leak by using @ObservedObject through UIHostingController rootView

I have the following scenario:

a SwiftUI view (very simple) which I need to use as inputAccessoryView for a UIKit UITextField. By tapping some buttons in the SwiftUI view, I need to update the backgroundColor of the UITextField.

I approached in this way. When I becomeFirstResponder the textField, I initialize the SwiftUI view embedded in a UIHostingController. The UIHostingController's view, is a subview of a UIInput view, which I use as inputAccessoryView for the textfield. In order to detect taps on the SwiftUI view and then update the textField's backgroundColor, I'm using an ObservableObject which has a Published String property. This string is the "identifier" of the color I use to update the textField's background color.

So, I have the UIInputView. I'm passing it the ObservableObject and inject it into the SwiftUIView as ObservedObject. After the initialization of these things, I observe the changes of the Published String property by using a simple AnyCancellable. I'm doing this in the method which triggers the textField with the becomeFirstResponder(). Everything works fine, but there's a problem.

After the dismissing of the textField, the keyboard and the related UIInputView are gone. By opening the memory graph I don't see any instance of the UIInputView. But I see an instance of the ObservableObject and an instance of the SwiftUI view. I always have +1 in memory of the ObservableObject and the SwiftUI view for each becomeFirstResponder() and resignFirstResponder() I call on the textField. I'm struggling on this and I don't understand what I'm doing wrong. I paste some pieces of code...

func triggerTextField() {
  let viewModel = MyViewModel(...)
  let inputAccessoryView = MyInputView(viewModel: viewModel)
  self.textField.inputAccessoryView = inputAccessoryView
  self.textField.becomeFirstResponder()

  cancellable = viewModel.$myColorString.sink { [weak self] newValue in
    self?.textField.backgroundColor = UIColor(newValue)
  }
}
class MyViewModel: ObservableObject {
  Published var myValue = String()
  ...
}
class MyInputView: UIInputView {

  init(viewModel: MyViewModel) {
    super.init(frame: .zero, inputViewStyle: .keyboard)

    let mySwiftUIView = MySwiftUIView(viewModel: viewModel!)
    let hostingController = UIHostingController(rootView: mySwiftUIView)
    hostingController.view.translatesAutoresizingMaskIntoConstraints = false
    self.addSubview(hostingController.view)

    NSLayoutConstraint.activate([
      hostingController.view.topAnchor.constraint(equalTo: self.topAnchor),
      hostingController.view.leadingAnchor.constraint(equalTo: self.leadingAnchor),
      hostingController.view.trailingAnchor.constraint(equalTo: self.trailingAnchor),
      hostingController.view.bottomAnchor.constraint(equalTo: self.bottomAnchor)
    ])
  }
}
struct MySwiftUIView: View {
  
  ObservedObject var viewModel: MyViewModel
    
  var body: some View {
    HStack {
      ForEach(viewModel.values, id: \.self) { value in
        MyView(value: $viewModel.myValue)
          .onTapGesture {
            viewModel.myValue = value
          }
      }
    }
    .padding(12.0)
    .frame(height: 56.0)
    .frame(maxWidth: .infinity)
  }
}

I'd like to destroy from memory both the ViewModel and the SwiftUIView when I resignFirstResponder() the textField. Actually in the memory graph I see an instance of the viewModel which seems to have a strong reference to the SwiftUIView (rootView of the UIHostingController) and the Published string ?! I don't know why... I also have an instance of the SwiftUIView. I tried to use the StateObject instead of the ObservedObject: nothing. I tried to move the initialization of the ViewModel in the viewDidLoad of the UIViewController in which I have the textField: nothing. Any idea or suggestion?

After another day of struggling, I've found the problem.

The memory leak is not related to the ObservedObject or the UIHostingController. I wrote the inputView in pure UIKit and the issue was there, again... The problem is the UIInputView. Apparently, there's a bug since iOS 16 which cause any inputAccessoryView to never be deallocated. The same code with iOS 15.5 simulator works fine!

I tried also with Xcode 15 beta (6) and the issue persists. Hope Apple will fix this.

2 Likes