I have a custom UIViewControllerRepresentable
(layout-related code shown below). This tries to replicate the native SwiftUI ScrollView
, except it scrolls from the bottom except the top.
View hierarchy
view: UIView
|
\- scrollView: UIScrollView
|
\- innerView: UIView
|
\- hostingController.view: SwiftUI hosting view
This all works as intended when the view is initialized. The hosting view is populated with its contents, and the constraints make sure that the scroll view's contentSize
is set properly.
However, when the contents of the hosting view changes, the hostingController.view
doesn't resize to fit its contents.
Green: As intended, the scroll view matches the size of the hosting view controller.
Blue: The hosting view itself. It keeps the size it had when it was first loaded, and doesn't expend as it should.
Red: A stack view within the hosting view. In this screenshot, content was been added to the stack, causing it to expand. You can see the difference in size as a result.
The UIHostingController (blue) should expand to fit its contents (red).
The scroll view's content size is not explicitly set, because this is handled by auto layout.
Constraint code is shown below, if it helps.
class UIBottomScrollViewController<Content: View>: UIViewController, UIScrollViewDelegate {
var hostingController: UIHostingController<Content>! = nil
init(rootView: Content) {
self.hostingController = UIHostingController<Content>(rootView: rootView)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var scrollView: UIScrollView = UIScrollView()
var innerView = UIView()
override func loadView() {
self.view = UIView()
self.addChild(hostingController)
view.addSubview(scrollView)
scrollView.addSubview(innerView)
innerView.addSubview(hostingController.view)
scrollView.delegate = self
scrollView.scrollsToTop = true
scrollView.isScrollEnabled = true
scrollView.clipsToBounds = false
scrollView.layoutMargins = .zero
scrollView.preservesSuperviewLayoutMargins = true
scrollView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
scrollView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
scrollView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
innerView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
innerView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
innerView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
innerView.leftAnchor.constraint(equalTo: scrollView.leftAnchor).isActive = true
innerView.rightAnchor.constraint(equalTo: scrollView.rightAnchor).isActive = true
innerView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
hostingController.view.topAnchor.constraint(equalTo: innerView.topAnchor).isActive = true
hostingController.view.leftAnchor.constraint(equalTo: innerView.leftAnchor).isActive = true
hostingController.view.rightAnchor.constraint(equalTo: innerView.rightAnchor).isActive = true
hostingController.view.bottomAnchor.constraint(equalTo: innerView.bottomAnchor).isActive = true
hostingController.view.autoresizingMask = []
hostingController.view.layoutMargins = .zero
hostingController.view.insetsLayoutMarginsFromSafeArea = false
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
scrollView.autoresizingMask = []
scrollView.layoutMargins = .zero
scrollView.insetsLayoutMarginsFromSafeArea = false
scrollView.translatesAutoresizingMaskIntoConstraints = false
innerView.autoresizingMask = []
innerView.layoutMargins = .zero
innerView.insetsLayoutMarginsFromSafeArea = false
innerView.translatesAutoresizingMaskIntoConstraints = false
hostingController.didMove(toParent: self)
scrollView.keyboardDismissMode = .interactive
}
}
struct BottomScrollView<Content: View>: UIViewControllerRepresentable {
var content: () -> Content
init(@ViewBuilder content: @escaping () -> Content) {
self.content = content
}
func makeUIViewController(context: Context) -> UIBottomScrollViewController<Content> {
let vc = UIBottomScrollViewController(rootView: self.content())
return vc
}
func updateUIViewController(_ viewController: UIBottomScrollViewController<Content>, context: Context) {
viewController.hostingController.rootView = self.content()
}
}