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 anintrinsicContentSize
. SwiftUI relies on this property to determine aUIView
'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
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
Cllocation Distancefromlocation (In Swift)
Add Datepicker in Uiactionsheet Using Swift
How to Keep a Reference to Another Object in the Parameters of the Class
Cannot Subscript a Value of Type '[String:String]' with an Index of Type 'String'
How to Override Private Method and Call Super in Swift
A Swiftier Way to Convert String to Unsafepointer<Xmlchar> in Swift 3 (Libxml2)
Create JSON Object from Class in Swift
How to Make a Button Draggable/Movable with Swiftui
Swift Client and Root Ssl Certificate Authentication
How to Deal with Buffered Strings from C in Swift
How to Get Data from a Swift Nsurlsession
Text Overlapping Itself in Table Cells Swift
How to Return a Button from a Function in Swiftui
Ckcontainer.Discoverallidentities Always Fails
What Is the Markup Format for Documentation on the Parameters of a Block in Swift