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:
UIHostingController
within the Coordinator, while Version B uses makeUIView
to do the same.TextField
, bound to a @State
property.ContainerViewA
are properly shown in A and B.ContainerViewB
are only shown in A.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)")
}
}
}
}
It's not that ContainerViewB
"blocks bindings". @Binding
s do work here. The @State
s are indeed being updated. This can be shown using the following code:
VStack {
ContainerViewB {
TextField("TextB", text: $textB)
}
Text(textB)
}
The Text
outside of ContainerViewB
is properly updated.
What went wrong in your code is that the views displaying the @State
s (the Text
s in ContainerViewB
) are not being updated when they should.
When SwiftUI detects a change, it will update your View
s by calling body
, and your UIViewRepresentable
s by calling updateUIView
. In body
, you are supposed to construct a new View
that reflects the new state. In updateUIView
, you are supposed to configure the UIView
that you are wrapping, so that it reflects the new state.
Here is what happens when you type in the second text field. SwiftUI detects a change in the state textB
, so calls ContainerTestView.body
. In there, you create a new ContainerViewB
with an updated content
(a TupleView
containing a TextField
and 2 Text
s). Since ContainerTestView
is not Equatable
, SwiftUI assumes that it has changed, and it calls ContainerViewB.updateUIView
, where you do nothing.
So the new content
you created in the view builder closure of ContainerViewB
are just stored in the content
property, and that's it. They are not displayed in the hosting controller.
Compare this to ContainerViewA
, where you do update the hosting controller in updateUIView
, so that it displays the new content
. This is exactly what you are supposed to do in updateUIView
.