How do I make my SwiftUI UIViewRepresentable respect intrinsicContentSize in previews?

11.7k Views Asked by At

When I create a view in SwiftUI and render it in an Xcode preview, using PreviewLayout.sizeThatFits, the preview adjusts its size according to its content. When I import a UIKIt view using UIViewRepresentable, it appears with a full device-size frame.

Is there a way to make SwiftUI respect the intrinsicContentSize of UIView subclasses?

struct Label: UIViewRepresentable {

    func makeUIView(context: UIViewRepresentableContext<Label>) -> UILabel {
        return UILabel()
    }

    func updateUIView(_ uiView: UILabel, context: UIViewRepresentableContext<Label>) {
        uiView.text = "Some text"
    }
}

#if DEBUG
struct Label_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            Label().previewLayout(.sizeThatFits)
        }
    }
}
#endif
3

There are 3 best solutions below

2
dwitt On

Add the following to your updateUIView function:

uiView.setContentHuggingPriority(.defaultHigh, for: .vertical)
uiView.setContentHuggingPriority(.defaultHigh, for: .horizontal)
0
pawello2222 On

You can also limit the UIViewRepresentable size from the SwiftUI side.

For this you can use fixedSize:

struct Label_Previews: PreviewProvider {
    static var previews: some View {
        Label()
            .fixedSize()
            .previewLayout(.sizeThatFits)
    }
}

You can also fix the view size in one dimension only:

.fixedSize(horizontal: false, vertical: true)
0
David On

Using Swift 5, SwiftUI 15, the following is a solution that works for a view hierarchy using layout constraints.

The key is to pass the representable a binding that the SwiftUI view uses to set the height of the view. The representable sets the value of the height by asking the system for the minimum size that fits.

This works for both UIViewRepresentable and UIViewControllerRepresentable.

struct WrappingView: UIViewControllerRepresentable {
    @Binding var height: CGFloat
    
    func makeUIViewController(context: Context) -> WrappedViewController {   
        return WrappedViewController()
    }
    
    func updateUIViewController(_ uiViewController: WrappedViewController, context: Context) {

        DispatchQueue.main.async {
            height = uiViewController.view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height
        }
    }
    
    typealias UIViewControllerType = WrappedViewController
}

Then in the SwiftUI view, set the frame height to match the value from the representable.

struct MixedView: View {
    @State private var wrappedViewHeight: CGFloat = 0
   
    var body: some View {
        VStack {
            WrappingView(height: $wrappedViewHeight)
            .frame(height: wrappedViewHeight)
        }
    }
}

Sample wrapped view controller for trivial case

class WrappedViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        let label = UILabel()
        label.text = "Testing"
        label.backgroundColor = .lightGray
        label.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(label)
        NSLayoutConstraint.activate([
            label.topAnchor.constraint(equalTo: view.topAnchor),
            label.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
    }
}

And in Xcode 15, a preview is as simple as

#Preview {
    MixedView()
}