swiftswiftuilayoutautolayoutuiviewrepresentable

SwiftUI's layout engine does not respect content size of UIViewRepresentable and expands size to whole screen


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.


Solution

  • 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
                )
            )
        }
    }
    

    Result look like this: enter image description here