0

So there seems to be a retain cycle when injecting a Binding that is a published property from an ObservableObject into UIViewControllerRepresentable.

It seems if you create a view inside another view in and that second view has an ObservableObject and injects it's published property into the UIViewControllerRepresentable and is then used in the coordinator, the ObservableObject is never released when the original view is refreshed.

Also it looks like the Binding gets completely broken and the UIViewControllerRepresentable no longer works

When looking at it, it makes sense that Coordinator(self) is bad, but Apple's own documentation says to do it this way. Am I missing something?

Here is a quick example:

struct ContentView: View {
    @State var resetView: Bool = true
    var body: some View {
        VStack {
            OtherView()
            Text("\(resetView ? 1 : 0)")
            // This button just changes the state to refresh the view
            // Also after this is pressed the UIViewControllerRepresentable no longer works
            Button(action: {resetView.toggle()}, label: {
                Text("Refresh")
            })
        }
    }
}

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

struct OtherView: View {
    @ObservedObject var viewModel = OtherViewModel()
    var body: some View {
        VStack {
            Text("Value: \(viewModel.value)")
            Wrapper(value: $viewModel.value).frame(width: 100, height: 50, alignment: .center)
        }
    }
}

class OtherViewModel: ObservableObject {
    @Published var value: Int
    
    deinit {
        print("OtherViewModel deinit") // this never gets called
    }
    
    init() {
        self.value = 0
    }
}

struct Wrapper: UIViewControllerRepresentable {
    @Binding var value: Int
    class Coordinator: NSObject, ViewControllerDelegate {
        var parent: Wrapper
        init(_ parent: Wrapper) {
            self.parent = parent
        }
        func buttonTapped() {
            // After the original view is refreshed this will no longer work
            parent.value += 1
        }
    }
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    func makeUIViewController(context: Context) -> ViewController {
        let vc = ViewController()
        vc.delegate = context.coordinator
        return vc
    }
    func updateUIViewController(_ uiViewController: ViewController, context: Context) {}
}

protocol ViewControllerDelegate: class {
    func buttonTapped()
}

class ViewController: UIViewController {
    weak var delegate: ViewControllerDelegate?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        let button = UIButton(frame: CGRect(x: 0, y: 0, width: 100, height: 20))
        button.setTitle("increment", for: .normal)
        button.setTitleColor(UIColor.blue, for: .normal)
        button.addTarget(self, action: #selector(self.buttonTapped), for: .touchUpInside)
        self.view.addSubview(button)
    }
    @objc func buttonTapped(sender : UIButton) {
        delegate?.buttonTapped()
    }
}
wshamp
  • 131
  • 4
  • `OtherView` is always in view hierarchy, so why do you expect that `OtherViewModel`, being linked to view, would ever call deinit? – Asperi Mar 09 '21 at 04:50
  • When you tap the refresh button it will trigger the content view to redraw and that should recreate the view model. This in fact does happen if you don’t capture self in the coordinator. – wshamp Mar 09 '21 at 12:41
  • Also when you refresh content view, you can see two instances of that view model in the memory graph debugger instead of one. – wshamp Mar 09 '21 at 12:52

0 Answers0