I'm trying to incorporate a UIViewRepresentable into my SwiftUI app. My goal is that the view is displayed with the dimensions (especially height) it needs to fit the content. However, SwiftUI somehow does not respect the height of my UIViewRepresentable view. It just keeps expanding until the entire vertical space is filled.
I've already done a lot of research, but no solution I found could fix the issue for my specific case. The topic seems rather complex.
Below is a simple example that demonstrates my problem. Any hints would be much appreciated!
import SwiftUI
struct ContentView: View {
var body: some View {
GeometryReader { geometry in
let width = geometry.size.width
let height = width / 0.9
CustomCardView {
ZStack(alignment: .bottom) {
Image(systemName: "square.and.arrow.up.circle.fill")
.resizable()
.scaledToFill()
.frame(width: width, height: height)
.foregroundStyle(.red)
/*
VStack {
Text("This is a very long test text that tells us basically nothing and only serves as an example")
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.multilineTextAlignment(.leading)
.lineLimit(2)
.font(.title)
.fontWeight(.bold)
Text("This is a very long test text that tells us basically nothing and only serves as an example")
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.multilineTextAlignment(.leading)
.lineLimit(2)
.font(.body)
}
.background(.ultraThinMaterial)
*/
CustomView()
.padding()
.background(.ultraThinMaterial)
}
}
}
.padding([.horizontal, .bottom])
}
}
struct CustomView: UIViewRepresentable {
func makeUIView(context: Context) -> some UIView {
let rootView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = true
view.backgroundColor = .clear
return view
}()
let titleLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 2
label.translatesAutoresizingMaskIntoConstraints = false
label.backgroundColor = .yellow
return label
}()
let contentView: UITextView = {
let textView = UITextView()
textView.isEditable = false
textView.isSelectable = true
textView.isScrollEnabled = false
textView.translatesAutoresizingMaskIntoConstraints = false
textView.textContainerInset = .zero
textView.textContainer.lineFragmentPadding = .zero
textView.textContainer.maximumNumberOfLines = 3
textView.backgroundColor = .brown
textView.textContainer.lineBreakMode = .byTruncatingTail
return textView
}()
rootView.addSubview(titleLabel)
rootView.addSubview(contentView)
NSLayoutConstraint.activate([
titleLabel.topAnchor.constraint(equalTo: rootView.topAnchor),
titleLabel.leadingAnchor.constraint(equalTo: rootView.leadingAnchor),
titleLabel.trailingAnchor.constraint(equalTo: rootView.trailingAnchor),
contentView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 16),
contentView.leadingAnchor.constraint(equalTo: rootView.leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: rootView.trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: rootView.bottomAnchor)
])
titleLabel.text = "This is a very long test text that tells us basically nothing and only serves as an example"
contentView.text = "This is a very long test text that tells us basically nothing and only serves as an example"
return rootView
}
func updateUIView(_ uiView: UIViewType, context: Context) {
// not necessary
}
}
struct CustomCardView<Content: View>: View {
let content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
content
.frame(maxWidth: .infinity)
.background(Color.green)
.cornerRadius(16)
.shadow(color: .black.opacity(0.4), radius: 8, x: 0, y: 5)
}
}
#Preview {
ContentView()
}
Here's a screenshot of my desired behavior using a SwiftUI Text. Just remove the comment in the code below, and comment out the CustomView() part.
Here's screenshot of the undesired behaviour using UIViewRepresentable. The card view is expanded vertically to the full screen size, even if the text is really short. I've applied some background color to the elements of the UIViewRepresentable so it's easier to see that they are way too big.
We must inform SwiftUI about UIView Size using the sizeThatFits
function because SwiftUI is unaware of it.
import SwiftUI
struct ContentView: View {
var body: some View {
GeometryReader { geometry in
let width = geometry.size.width
let height = width / 0.9
CustomCardView {
ZStack(alignment: .bottom) {
Image(systemName: "square.and.arrow.up.circle.fill")
.resizable()
.scaledToFill()
.frame(width: width, height: height)
.foregroundStyle(.red)
CustomView()
.frame(maxWidth: .infinity)
.padding()
.background(.ultraThinMaterial)
}
}
}
.padding([.horizontal, .bottom])
}
}
class CustomUIKitView: UIView {
private var titleLabel: UILabel!
private var contentView: UITextView!
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupView() {
titleLabel = UILabel()
titleLabel.numberOfLines = 2
titleLabel.backgroundColor = .yellow
titleLabel.translatesAutoresizingMaskIntoConstraints = false
contentView = UITextView()
contentView.isEditable = false
contentView.isSelectable = true
contentView.isScrollEnabled = false
contentView.translatesAutoresizingMaskIntoConstraints = false
contentView.textContainerInset = .zero
contentView.textContainer.lineFragmentPadding = .zero
contentView.textContainer.maximumNumberOfLines = 3
contentView.backgroundColor = .brown
contentView.textContainer.lineBreakMode = .byTruncatingTail
contentView.setContentCompressionResistancePriority(.required, for: .vertical)
addSubview(titleLabel)
addSubview(contentView)
NSLayoutConstraint.activate([
titleLabel.topAnchor.constraint(equalTo: topAnchor),
titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
contentView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 16),
contentView.leadingAnchor.constraint(equalTo: leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: bottomAnchor)
])
titleLabel.text = "This is a very long test text that tells us basically nothing and only serves as an example"
contentView.text = "This is a very long test text that tells us basically nothing and only serves as an example"
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
let titleLabelSize = titleLabel.sizeThatFits(size)
let contentViewSize = contentView.sizeThatFits(size)
let totalHeight = titleLabelSize.height + contentViewSize.height + 16
let totalWidth = min(size.width, max(titleLabelSize.width, contentViewSize.width))
return CGSize(width: totalWidth, height: totalHeight)
}
}
struct CustomView: UIViewRepresentable {
func makeUIView(context: Context) -> UIView {
CustomUIKitView()
}
func updateUIView(_ uiView: UIView, context: Context) {}
func sizeThatFits(
_ proposal: ProposedViewSize,
uiView: UIView,
context: Context
) -> CGSize? {
uiView.sizeThatFits(
CGSize(
width: proposal.width ?? .infinity,
height: proposal.height ?? .infinity
)
)
}
}