import Foundation import SwiftUI import UIKit // MARK: - No container view (directly use UITextView) #Preview("Text view without container but inside UIStackView ✅") { let textView1 = makeTextView() textView1.text = line1 let textView2 = makeTextView() textView2.text = line2 let viewModel = AccordionView.ViewModel() viewModel.content = SomeUIViewRepresentable { UIStackView( axis: .vertical, arrangedSubviews: [ textView1, textView2, ] ) } let textView3 = makeTextView() textView3.text = line3 let textView4 = makeTextView() textView4.text = line4 viewModel.content2 = SomeUIViewRepresentable { UIStackView( axis: .vertical, arrangedSubviews: [ textView3, textView4, ] ) } let view = AccordionView(viewModel: viewModel) // Returning any of the below does size correctly ✅ return view // return UIHostingController(rootView: view) // return UIHostingController(intrinsicSizeRootView: view) // return HostedView(view: view) } #Preview("Text view without container ✅") { let textView1 = makeTextView() textView1.text = line1 let viewModel = AccordionView.ViewModel() viewModel.content = SomeUIViewRepresentable { textView1 } let textView3 = makeTextView() textView3.text = line3 let textView4 = makeTextView() textView4.text = line4 viewModel.content2 = SomeUIViewRepresentable { UIStackView( axis: .vertical, arrangedSubviews: [ textView3, textView4, ] ) } let view = AccordionView(viewModel: viewModel) // Returning any of the below does size correctly ✅ return view // return UIHostingController(rootView: view) // return UIHostingController(intrinsicSizeRootView: view) // return HostedView(view: view) } // MARK: - Container view (wrap UITextView in a UIView) #Preview("Text view inside container and all inside a UIStackView ⚠️") { let textView1 = makeTextView() textView1.text = line1 let container1 = TextViewContainer(uiView: textView1) let textView2 = makeTextView() textView2.text = line2 let container2 = TextViewContainer(uiView: textView2) let viewModel = AccordionView.ViewModel() viewModel.content = SomeUIViewRepresentable { UIStackView( axis: .vertical, arrangedSubviews: [ container1, container2, ] ) } let textView3 = makeTextView() textView3.text = line3 let textView4 = makeTextView() textView4.text = line4 viewModel.content2 = SomeUIViewRepresentable { UIStackView( axis: .vertical, arrangedSubviews: [ textView3, textView4, ] ) } let view = AccordionView(viewModel: viewModel) // Returning any of the below does size correctly ✅ return view // return UIHostingController(rootView: view) // Returning any of the below doesn't render the size correctly ❌ // return UIHostingController(intrinsicSizeRootView: view) // return HostedView(view: view) } #Preview("Text view inside container ❌") { let textView1 = makeTextView() textView1.text = line1 let container1 = TextViewContainer(uiView: textView1) let textView2 = makeTextView() textView2.text = line2 let container2 = TextViewContainer(uiView: textView2) let viewModel = AccordionView.ViewModel() viewModel.content = SomeUIViewRepresentable { container1 } let textView3 = makeTextView() textView3.text = line3 let textView4 = makeTextView() textView4.text = line4 viewModel.content2 = SomeUIViewRepresentable { UIStackView( axis: .vertical, arrangedSubviews: [ textView3, textView4, ] ) } let view = AccordionView(viewModel: viewModel) // Returning any of the below doesn't render the size correctly ❌ return view // return UIHostingController(rootView: view) // return UIHostingController(intrinsicSizeRootView: view) // return HostedView(view: view) } class HostedView: UIView { let hostingController: UIHostingController<content> convenience init(@ViewBuilder view: () -> Content) { self.init(view: view()) } init(view: Content) { hostingController = UIHostingController(intrinsicSizeRootView: view) super.init(frame: .zero) let view = hostingController.view! addSubview(view) translatesAutoresizingMaskIntoConstraints = false view.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ view.leadingAnchor.constraint(equalTo: leadingAnchor), view.trailingAnchor.constraint(equalTo: trailingAnchor), view.topAnchor.constraint(equalTo: topAnchor), view.bottomAnchor.constraint(equalTo: bottomAnchor), ]) // TODO: Not sure if these are needed here ... // // For VERTICAL stack views - height is critical // setContentHuggingPriority(.required, for: .vertical) // setContentCompressionResistancePriority(.required, for: .vertical) // // // Horizontal can be flexible (stack controls width) // setContentHuggingPriority(.defaultLow, for: .horizontal) // setContentCompressionResistancePriority(.defaultLow, for: .horizontal) } @available(*, unavailable) required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } } public extension UIHostingController { convenience init(intrinsicSizeRootView: Content) { self.init(rootView: intrinsicSizeRootView) sizingOptions = .intrinsicContentSize view.backgroundColor = .clear } } public struct AccordionView: View { let viewModel: ViewModel public init(viewModel: ViewModel) { self.viewModel = viewModel } @State var visible = false public var body: some View { VStack(spacing: 0) { Text("The Boy Who Lived") Button("tap me") { visible.toggle() } // TODO: Not sure if '.fixedSize()' is needed here or not... viewModel .content? // .fixedSize(horizontal: false, vertical: true) .background(.secondary) if visible { viewModel .content2? // .fixedSize(horizontal: false, vertical: true) .background(.tertiary) } } .padding(2) .border(.red, width: 2) } } public extension AccordionView { @MainActor @Observable class ViewModel { public var content: SomeUIViewRepresentable? public var content2: SomeUIViewRepresentable? init() { } } } func makeTextView() -> UILabel { // Simplified to UILabel but UITextView with the below properties also has the same work/not working pattern... this means it MUST be an issude with a wrapper view not reporting the correct intrinsic content size/system layout fitting size? ... let textView = UILabel() textView.numberOfLines = 0 // let textView = UITextView() // textView.backgroundColor = .red.withAlphaComponent(0.3) // // textView.isEditable = false // textView.isScrollEnabled = false // textView.font = .systemFont(ofSize: 16) // // textView.textContainer.maximumNumberOfLines = 0 // textView.textContainer.lineBreakMode = .byWordWrapping // textView.textContainerInset = .zero // textView.textContainer.widthTracksTextView = true // textView.textContainer.lineFragmentPadding = 0 return textView } public extension UIStackView { convenience init( axis: NSLayoutConstraint.Axis = .vertical, arrangedSubviews: [UIView] = [] ) { self.init(frame: .zero) for subview in arrangedSubviews { self.addArrangedSubview(subview) } self.axis = axis } } public final class TextViewContainer: UIView { let uiview: UIView public init(uiView: UIView) { self.uiview = uiView super.init(frame: .zero) layer.borderWidth = 1 layer.borderColor = UIColor.green.cgColor addSubview(uiView) translatesAutoresizingMaskIntoConstraints = false uiView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ uiView.leadingAnchor.constraint(equalTo: leadingAnchor), uiView.trailingAnchor.constraint(equalTo: trailingAnchor), uiView.topAnchor.constraint(equalTo: topAnchor), uiView.bottomAnchor.constraint(equalTo: bottomAnchor), ]) // TODO: Are these needed? // IMPORTANT: these must be on the arranged subview (the wrapper) // setContentHuggingPriority(.required, for: .vertical) // setContentCompressionResistancePriority(.required, for: .vertical) } required init?(coder: NSCoder) { fatalError() } public override func systemLayoutSizeFitting( _ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority ) -> CGSize { let width = targetSize.width let height = uiview.systemLayoutSizeFitting( .init( width: width, height: UIView.layoutFittingCompressedSize.height ), withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel ).height return CGSize(width: width, height: height) } } public struct SomeUIViewRepresentable: UIViewRepresentable { let makeContentView: () -> UIView public init(makeContentView: @escaping () -> UIView) { self.makeContentView = makeContentView } public func makeUIView(context _: Context) -> some UIView { makeContentView() } public func updateUIView(_ uiView: UIViewType, context _: Context) { uiView.invalidateIntrinsicContentSize() } public func sizeThatFits(_ proposal: ProposedViewSize, uiView: UIViewType, context _: Context) -> CGSize? { guard let width = proposal.width else { return uiView.systemLayoutSizeFitting( UIView.layoutFittingCompressedSize, withHorizontalFittingPriority: .fittingSizeLevel, verticalFittingPriority: .fittingSizeLevel ) } let targetSize = CGSize( width: width, height: UIView.layoutFittingCompressedSize.height ) let size = uiView.systemLayoutSizeFitting( targetSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel ) return CGSize(width: width, height: size.height) } } private let line1 = "Mr. and Mrs. Dursley, of number four, Privet Drive, were proud to say that they were perfectly normal, thank you very much." private let line2 = "They were the last people you’d expect to be involved in anything strange or mysterious, because they just didn’t hold with such nonsense." private let line3 = "Mr. Dursley was the director of a firm called Grunnings, which made drills. He was a big, beefy man with hardly any neck, although he did have a very large mustache." private let line4 = "Mrs. Dursley was thin and blonde and had nearly twice the usual amount of neck, which came in very useful as she spent so much of her time craning over garden fences, spying on the neighbors."</content>