How to Make Swiftui Uiviewrepresentable View Hug Its Content

UIViewRepresentable and its content's intrinsic size

After trying various techniques and hacks - I simply could not get the UIKit container (i.e. VibrantView) to hug its SwiftUI contents reliably, without adding a fixed sized .frame(...) modifier on top - which makes it difficult to use this with dynamically sized Text.

What did work for me was a bit of a hack and probably won't work for every generic view out there (and probably won't scale well for dozens of views), but works well for simple use cases, especially if you're hosting this inside of a dynamically sized UITableViewCell.

The idea is to use a dummy version of the same view, and set the VibrantView in an .overlay( ... ). This will force the overlay to assume the same overall size of the parent SwitfUI View. Since the view being applied the modifier is a copy of the same view that VibrantView wraps, you end up with the correct dynamic size at runtime and in Xcode previews.

So something like this:

  SomeView()
.frame(minWidth: 0, maxWidth: .infinity)
.overlay(
VibrantView(vibrancyBlurEffectStyle: .dark) {
SomeView()
.frame(minWidth: 0, maxWidth: .infinity)
}
)

I can imagine turning this into a modifier so that it wraps the above in a single call, but in my case to ensure it remains performant for Images, I'm doing something like this:

  Circle()
.foregroundColor(.clear)
.frame(width: 33, height: 33)
.overlay(
VibrantView(vibrancyBlurEffectStyle: .systemMaterialDark) {
Image("some image")
.resizable()
}
)

Creating a Circle is arguably lighter weight compared to the actual image. I create a transparent circle, set the actual size of the image there, and then put the VibrantView container into the overlay.

How do I get my UIView to correctly size for its content in SwiftUI?

Cannot test it now, just by thoughts, try with fixed size as below

MultiSegmentPicker(
selectedSegmentIndexes: $selectedSegmentIndexes,
items: ["First", "Second", "Third", "Done"]
).fixedSize() // << here !!

How to set the size of an UIViewRepresentable?

See: How does UIViewRepresentable size itself in relation to the UIKit control inside it?

struct SizeRepresentableTest: View {
var body: some View {
VStack {
SizeRepresentable().fixedSize()
Spacer()
}
}
}

iOS: Make function calls between UIViewRepresentable and View both ways, SwiftUI

You can use computed property and closure for a callback.

Here is the example code.

struct LoginView: View {

// Other code

// Webview var
private var webView: LoginWebview {
LoginWebview(testdo: self.showJSAlert) {
// Here you will get the call back
print("Callback")
}
}

var body: some View {

webView // <-- Here

// Other Code

And for button action

Button(action: {

//Calls login webview and triggers update that calls the js
self.showJSAlert.toggle()
// Access your function
self.webView.printstuff()

}) {
Text("Login")
.padding()
.font(.system(size: 20))

}

And in LoginWebview

struct LoginWebview: UIViewRepresentable {
var testdo = false

var callBack: (() -> Void)? = nil
class Coordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler {
// Other code

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
self.control.callBack?() // < Example for calling callback
}

// Other code
}

Multiline label with UIViewRepresentable

There's a few things going on here.

  • InformationView doesn't have an intrinsicContentSize. SwiftUI relies on this property to determine a UIView's ideal size.
  • No fixedSize modifier. This modifier forces a view to maintain its ideal size rather than grow to fill its parent.

You can't add an intrinsicContentSize because the content size is dynamic; it's based on the number of lines in your label.

You can't add the fixedSize modifier, because without an intrinsicContentSize, this will set the size to (0,0).

/p>

One solution is to wrap InformationView in a UIView that measures the InformationView size, and updates its own intrinsicContentSize according to that measurement.

Your InformationView should fill the width of the screen; it doesn't have an intrinsic width. On the other hand, the intrinsic height should be equal to the height of the compressed system layout size. This is "the optimal size of the view based on its constraints".

Here is the wrapper:

class IntrinsicHeightView<ContentView: UIView>: UIView {
var contentView: ContentView

init(contentView: ContentView) {
self.contentView = contentView
super.init(frame: .zero)
backgroundColor = .clear
addSubview(contentView)
}

@available(*, unavailable) required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

private var contentHeight: CGFloat = .zero {
didSet { invalidateIntrinsicContentSize() }
}

override var intrinsicContentSize: CGSize {
.init(width: UIView.noIntrinsicMetric, height: contentHeight)
}

override var frame: CGRect {
didSet {
guard frame != oldValue else { return }

contentView.frame = self.bounds
contentView.layoutIfNeeded()

let targetSize = CGSize(width: frame.width, height: UIView.layoutFittingCompressedSize.height)

contentHeight = contentView.systemLayoutSizeFitting(
targetSize,
withHorizontalFittingPriority: .required,
verticalFittingPriority: .fittingSizeLevel).height
}
}
}

This IntrinsicHeightView is a generic UIView that, when its frame changes, recalculates the height of its intrinsicContentSize according to the compressed system layout height of its content view.

Here is the update to InformationRepresenter:

struct InformationRepresenter: UIViewRepresentable {
func makeUIView(context: Context) -> IntrinsicHeightView<InformationView> {
return IntrinsicHeightView(contentView: InformationView())
}

func updateUIView(_ uiView: IntrinsicHeightView<InformationView>, context: Context) {
}
}

And finally, when using it in SwiftUI:

InformationRepresenter()
.padding()
.fixedSize(horizontal: false, vertical: true)

Using the fixedSize modifier this way allows the UIView to fill the parent, but use intrinsicContentSize.height as its height. Here is the final result. Good luck!

Size a UILabel in SwiftUI via UIViewRepresentable like Text to wrap multiple lines

The problem here is in ScrollView which requires definite height, but representable does not provide it. The possible solution is to dynamically calculate wrapped text height and specify it explicitly.

Note: as height is calculated dynamically it is available only in run-time, so cannot be tested with Preview.

Tested with Xcode 12 / iOS 14

demo

struct LabelView: View {
var text: String

@State private var height: CGFloat = .zero

var body: some View {
InternalLabelView(text: text, dynamicHeight: $height)
.frame(minHeight: height)
}

struct InternalLabelView: UIViewRepresentable {
var text: String
@Binding var dynamicHeight: CGFloat

func makeUIView(context: Context) -> UILabel {
let label = UILabel()
label.numberOfLines = 0
label.lineBreakMode = .byWordWrapping
label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)

return label
}

func updateUIView(_ uiView: UILabel, context: Context) {
uiView.text = text

DispatchQueue.main.async {
dynamicHeight = uiView.sizeThatFits(CGSize(width: uiView.bounds.width, height: CGFloat.greatestFiniteMagnitude)).height
}
}
}
}


Related Topics



Leave a reply



Submit