UIHostingController should expand to fit contents
I encountered the same issue with a similar-ish view hierarchy involving UIHostingController
and scroll views, and found an ugly hack to make it work. Basically, I add a height constraint and update the constant manually:
private var heightConstraint: NSLayoutConstraint?
...
override func viewDidLoad() {
...
heightConstraint = viewHost.view.heightAnchor.constraint(equalToConstant: 0)
...
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// ð¬
viewHost.view.sizeToFit()
heightConstraint?.constant = viewHost.view.bounds.height
heightConstraint?.isActive = true
}
This is horrible code, but it's the only thing I found that made it work.
For me the solution was much simpler than any other answer I see here (none of which worked), though it took me quite some time to find it.
All I did was create a thin subclass of UIHostingController
that calls invalidateIntrinsicContentSize()
on its view in response to viewDidLayoutSubviews()
class SelfSizingHostingController<Content>: UIHostingController<Content> where Content: View {
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.view.invalidateIntrinsicContentSize()
}
}
Similar to the original question, I have a SwiftUI view that I am hosting inside of a UIViewController
in a UIScrollView
, which needs to be laid out with other views in the scrolling content view. The SwiftUI view's intrinsic size changes depending on its content and the user's chosen Dynamic Type size.
In my case it was really this simple. It works for me in iOS 14+ (not tested on iOS 13) where a change in the SwiftUI content that would result in a new intrinsic size correctly updates my autolayout-based UIKit layout in the scroll view. Honestly it feels like a bug that this isn't the implicit behavior of UIHostingController
.
I do not recommend using the SelfSizingHostingController. You can get an Auto Layout loop with it (I succeeded).
The best solution turned out to be to call invalidateIntrinsicContentSize()
immediately after setting the content.
Like here:
hostingController.rootView = content
hostingController.view.invalidateIntrinsicContentSize()
This plays off what @Rengers was saying, but wanted to include my solution that took me a fair amount of time to figure out.
Hopefully save some time
struct SizingView<T: View>: View {
let view: T
let updateSizeHandler: ((_ size: CGSize) -> Void)
init(view: T, updateSizeHandler: @escaping (_ size: CGSize) -> Void) {
self.view = view
self.updateSizeHandler = updateSizeHandler
}
var body: some View {
view.background(
GeometryReader { proxy in
Color.clear
.preference(key: SizePreferenceKey.self, value: proxy.size)
}
)
.onPreferenceChange(SizePreferenceKey.self) { preferences in
updateSizeHandler(preferences)
}
}
func size(with view: T, geometry: GeometryProxy) -> T {
updateSizeHandler?(geometry.size)
return view
}
}