The SwiftUI ScrollView lacks some features I need, so I used UIViewRepresentable to create a custom container based on UIScrollView. I found different tutorials showing how to create custom container views. However, while one solution works without any problem, another solution seems to block Bindings some how and I do not understand why.
So the question is, why does ContainerViewA work properly while ContainerViewB blocks bindings?
To keep things simple, the following example creates a simple UIView container instead of using UIScrollView, but the problem is the same:
- Version A creates the
UIHostingControllerwithin the Coordinator, while Version B usesmakeUIViewto do the same. - The TestView uses both versions, each containing a
TextField, bound to a@Stateproperty. - Text changes within
ContainerViewAare properly shown in A and B. - Text changes within
ContainerViewBare only shown in A. - Thus "incoming" bindings are not properly handled in
ContainerViewB. Why?
struct ContainerViewA<Content: View>: UIViewRepresentable {
let content: Content
@inlinable init(@ViewBuilder content: () -> Content) {
self.content = content()
}
func makeUIView(context: Context) -> UIView {
let view = UIView()
let hostingController = context.coordinator.hostingController
hostingController.view.frame = view.bounds
hostingController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(hostingController.view)
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
context.coordinator.hostingController.rootView = self.content
}
func makeCoordinator() -> Coordinator {
return Coordinator(hostingController: UIHostingController(rootView: content))
}
class Coordinator: NSObject {
var hostingController: UIHostingController<Content>
init(hostingController: UIHostingController<Content>) {
self.hostingController = hostingController
}
}
}
struct ContainerViewB<Content: View>: UIViewRepresentable {
var content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
func makeUIView(context: Context) -> UIView {
let view = UIView()
let hostingController = UIHostingController(rootView: content)
hostingController.view.frame = view.bounds
hostingController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(hostingController.view)
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
}
func makeCoordinator() -> Coordinator {
Coordinator(parent: self)
}
class Coordinator: NSObject, UIScrollViewDelegate {
private var parent: ContainerViewB
init(parent: ContainerViewB) {
self.parent = parent
super.init()
}
}
}
struct ContainerTestView: View {
@State private var textA: String = ""
@State private var textB: String = ""
var body: some View {
VStack {
ContainerViewA {
TextField("TextA", text: $textA)
Text("Typed A: \(textA)")
Text("Typed B: \(textB)")
}
ContainerViewB {
TextField("TextB", text: $textB)
Text("Typed A: \(textA)")
Text("Typed B: \(textB)")
}
}
}
}

UIHostingControllerto the view hierarchy of anotherUIViewControllerwithout informing the parent view controller of the process. That's sth. you shouldn't do. Instead you should use aUIViewControllerRepresentableinstead of aUIViewRepresentableand follow Apple's guide to Creating a custom container view controller to add the view of yourUIHostingControllerinstance to the view hierarchy.