Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I make SwiftUI UIViewRepresentable view hug its content?

Tags:

swift

swiftui

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: enter image description here

Is there a way for me to have it do this like I would expect? enter image description here

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...

like image 501
ccwasden Avatar asked Oct 01 '19 12:10

ccwasden


2 Answers

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:

  1. You must specify .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.
  2. You have to set 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.

like image 152
Minsheng Liu Avatar answered Sep 20 '22 07:09

Minsheng Liu


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)
   }
}
like image 39
Rohit Makwana Avatar answered Sep 22 '22 07:09

Rohit Makwana