I am trying to get SwiftUI to recognize the intrinsic size of a UIViewRepresentable, but it seems to treat it like the frame is set to maxWidth: .infinity, maxHeight: .infinity
. I have created a contrived example, here is my code:
struct Example: View {
var body: some View {
VStack {
Text("Hello")
TestView()
}
.background(Color.red)
}
}
struct TestView: UIViewRepresentable {
func makeUIView(context: UIViewRepresentableContext<TestView>) -> TestUIView {
return TestUIView()
}
func updateUIView(_ uiView: TestUIView, context: UIViewRepresentableContext<TestView>) {
}
typealias UIViewType = TestUIView
}
class TestUIView: UIView {
required init?(coder: NSCoder) { fatalError("-") }
init() {
super.init(frame: .zero)
let label = UILabel()
label.text = "Sed mattis urna a ipsum fermentum, non rutrum lacus finibus. Mauris vel augue lorem. Donec malesuada non est nec fermentum. Integer at interdum nibh. Nunc nec arcu mauris. Suspendisse efficitur iaculis erat, ultrices auctor magna."
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
label.backgroundColor = .purple
addSubview(label)
translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
widthAnchor.constraint(equalToConstant: 200),
label.leadingAnchor.constraint(equalTo: leadingAnchor),
label.trailingAnchor.constraint(equalTo: trailingAnchor),
label.topAnchor.constraint(equalTo: topAnchor),
label.bottomAnchor.constraint(equalTo: bottomAnchor),
])
}
}
struct Example_Previews: PreviewProvider {
static var previews: some View {
Example()
.previewDevice(PreviewDevice(rawValue: "iPhone 8"))
.previewLayout(PreviewLayout.fixed(width: 300, height: 300))
}
}
What I am getting:
Is there a way for me to have it do this like I would expect?
EDIT:
After further inspection, it seems I am getting Unable to simultaneously satisfy constraints.
and one of the constraints is a 'UIView-Encapsulated-Layout-Height'
constraint to the stretched out size. So I guess it might help to prevent those constraints from being forced upon my view? Not sure...
Use .fixedSize()
solves the problem for me.
What I did uses UIViewController
, so things might be a different for UIView
. What I tried to achieve is to do a sandwich structure like:
SwiftUI => UIKit => SwiftUI
Consider a simple SwiftUI view:
struct Block: View {
var text: String
init(_ text: String = "1, 2, 3") {
self.text = text
}
var body: some View {
Text(text)
.padding()
.background(Color(UIColor.systemBackground))
}
}
The background color is set to test whether outer SwiftUI's environment values pass to the inner SwiftUI. The general answer is yes, but caution is required if you use things like .popover
.
One nice thing about the UIHostingController is that you can get the SwiftUI's natural size using .intrinsicContentSize
. This works for tight views such as Text
and Image
, or a container containing such tight views. However, I am not sure what will happen with a GeometryReader
.
Consider the following view controller:
class VC: UIViewController {
let hostingController = UIHostingController(rootView: Block("Inside VC"))
var text: String = "" {
didSet {
hostingController.rootView = Block("Inside VC: \(text).")
}
}
override func viewDidLoad() {
addChild(hostingController)
view.addSubview(hostingController.view)
view.topAnchor.constraint(equalTo: hostingController.view.topAnchor).isActive = true
view.bottomAnchor.constraint(equalTo: hostingController.view.bottomAnchor).isActive = true
view.leadingAnchor.constraint(equalTo: hostingController.view.leadingAnchor).isActive = true
view.trailingAnchor.constraint(equalTo: hostingController.view.trailingAnchor).isActive = true
view.translatesAutoresizingMaskIntoConstraints = false
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
}
override func viewDidLayoutSubviews() {
preferredContentSize = view.bounds.size
}
}
Nothing is particularly interesting here. We turn off autoresizing masks for BOTH our UIView
and SwiftUI's hosting UIView
. We force our view to have the same natural size as SwiftUI's.
I would talk about updating preferredContentSize
later.
A boring VCRep
:
struct VCRep: UIViewControllerRepresentable {
var text: String
func makeUIViewController(context: Context) -> VC {
VC()
}
func updateUIViewController(_ vc: VC, context: Context) {
vc.text = text
}
typealias UIViewControllerType = VC
}
Now comes to the ContentView, i.e. the outer SwiftUI:
struct ContentView: View {
@State var alignmentIndex = 0
@State var additionalCharacterCount = 0
var alignment: HorizontalAlignment {
[HorizontalAlignment.leading, HorizontalAlignment.center, HorizontalAlignment.trailing][alignmentIndex % 3]
}
var additionalCharacters: String {
Array(repeating: "x", count: additionalCharacterCount).joined(separator: "")
}
var body: some View {
VStack(alignment: alignment, spacing: 0) {
Button {
self.alignmentIndex += 1
} label: {
Text("Change Alignment")
}
Button {
self.additionalCharacterCount += 1
} label: {
Text("Add characters")
}
Block()
Block().environment(\.colorScheme, .dark)
VCRep(text: additionalCharacters)
.fixedSize()
.environment(\.colorScheme, .dark)
}
}
}
And magically, everything works. You can change the horizontal alignment or add more characters to the view controller, and expect the correct layout.
Something to notice:
.fixedSize()
to use the natural size. Otherwise the view controller would take as much space as possible, just like a GeometryReader(). This is probably unsurprising given .fixedSize()
's documentation, but the naming is horrible. I would never expect .fixedSize()
to mean this.preferredContentSize
and keep it updated. Initially I found it had no visual impact on my code, but the horizontal alignment seems wrong because the VCRep
always has its leading anchor used as the layout guide for the VStack
. After checking the view dimensions via .alignmentGuide
, it turns out that VCRep
has a size zero. It still shows! Since no content clipping is enabled by default!By the way, if you have a situation where view height depends on its width, and you are lucky enough to be able to obtain the width (via GeometryReader
), you could also try to do layout on your own directly in a SwiftUI view's body
, and attaching your UIView as a .background
to serve as a custom painter. This works, for instance, if you use TextKit to compute your text layout.
Please try below code it will help you
// **** For getting label height ****
struct LabelInfo {
func heightForLabel(text: String, font: UIFont, width: CGFloat) -> CGFloat {
let label:UILabel = UILabel(frame: CGRect(x: 0, y: 0, width: width, height: CGFloat.greatestFiniteMagnitude))
label.numberOfLines = 0
label.lineBreakMode = NSLineBreakMode.byWordWrapping
label.font = font
label.text = text
label.sizeToFit()
return label.frame.height
}
}
Change in Example
struct Example: View {
// Note : Width is fixed 200 at both place
// : use same font at both
let height = LabelInfo().heightForLabel(text: "Sed mattis urna a ipsum fermentum, non rutrum lacus finibus. Mauris vel augue lorem. Donec malesuada non est nec fermentum. Integer at interdum nibh. Nunc nec arcu mauris. Suspendisse efficitur iaculis erat, ultrices auctor magna.", font: UIFont.systemFont(ofSize: 17), width: 200)
var body: some View {
VStack {
Text("Hello")
TestView().frame(width: 200, height: height, alignment: .center) //You need to do change here
}
.background(Color.red)
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With