Uiviewrepresentable Automatic Size - Passing UIkit UIview Size to Swiftui

UIViewRepresentable automatic size - Passing UIKit UIView size to SwiftUI

The solution is to set explicitly compression/hugging priority for represented UIView

Tested with Xcode 11.4 / iOS 13.4

struct YellowBoxView : UIViewRepresentable {

func makeUIView(context: Context) -> YellowBoxUIKitView {
let view = YellowBoxUIKitView()

view.setContentHuggingPriority(.required, for: .horizontal) // << here !!
view.setContentHuggingPriority(.required, for: .vertical)

// the same for compression if needed

return view
}

func updateUIView(_ uiView: YellowBoxUIKitView, context: Context) {
}
}

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

Most simple and quick fix:

demo

        SwiftUIText(text: $helperText).fixedSize()

Update:

demo2

        SwiftUIText(text: $helperText)
.fixedSize(horizontal: false, vertical: true)

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

UIHostingController size too big

The simplest way is to use SwiftUI-Introspect and just grab the UIView from it. This is all the code needed:

Text("This is some really long text that will have to wrap to multiple lines")
.introspect(selector: TargetViewSelector.siblingOfType) { target in
target.backgroundColor = .systemRed
}

If the view is a bit more complex and there isn't a UIView specifically for it, you can embed it in a ScrollView so the content will now be a UIView:

ScrollView {
Text("Complex content here")
}
.introspectScrollView { scrollView in
scrollView.isScrollEnabled = false
scrollView.clipsToBounds = false

scrollView.subviews.first!.backgroundColor = .systemRed
}

If you don't want to use Introspect (which I would highly recommend), there is a second solution below. The second solution works in most situations, but not all.


See solution above first.

I've created a working answer. It looks quite complicated, but it works.

It basically works by using the inside GeometryReader to measure the size of the content to be wrapped and the outside GeometryReader to measure the size of the whole container. This means that Text will now wrap lines because it's constrained by the outside container's size.

Code:

struct ContentView: View {
var body: some View {
Wrapper {
Text("This is some really long text that will have to wrap to multiple lines")
}
}
}
struct Wrapper<Content: View>: View {
@State private var size: CGSize?
@State private var outsideSize: CGSize?
private let content: () -> Content

init(@ViewBuilder content: @escaping () -> Content) {
self.content = content
}

var body: some View {
GeometryReader { outside in
Color.clear.preference(
key: SizePreferenceKey.self,
value: outside.size
)
}
.onPreferenceChange(SizePreferenceKey.self) { newSize in
outsideSize = newSize
}
.frame(width: size?.width, height: size?.height)
.overlay(
outsideSize != nil ?
Representable {
content()
.background(
GeometryReader { inside in
Color.clear.preference(
key: SizePreferenceKey.self,
value: inside.size
)
}
.onPreferenceChange(SizePreferenceKey.self) { newSize in
size = newSize
}
)
.frame(width: outsideSize!.width, height: outsideSize!.height)
.fixedSize()
.frame(width: size?.width ?? 0, height: size?.height ?? 0)
}
.frame(width: size?.width ?? 0, height: size?.height ?? 0)
: nil
)
}
}
struct SizePreferenceKey: PreferenceKey {
static let defaultValue: CGSize = .zero

static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}
struct Representable<Content: View>: UIViewRepresentable {
private let content: () -> Content

init(@ViewBuilder content: @escaping () -> Content) {
self.content = content
}

func makeUIView(context: Context) -> UIView {
let host = UIHostingController(rootView: content())
let hostView = host.view!
return hostView
}

func updateUIView(_ uiView: UIView, context: Context) {
uiView.backgroundColor = .systemRed
}
}

Result:

Result

Another example to show that it does make the wrapper the exact size as the SwiftUI view:

struct ContentView: View {
var body: some View {
VStack {
Wrapper {
Text("This is some really long text that will have to wrap to multiple lines")
}
.border(Color.green, width: 3)

Wrapper {
Text("This is some really long text that will have to wrap to multiple lines. However, this bottom text is a bit longer and may wrap more lines - but this isn't a problem here")
}
.border(Color.blue, width: 3)
}
}
}

Multiple wrappers

Set custom UIView frame in UIViewRepresentable SwiftUI

If MyView has correct internal layout (which depends only on own bounds), then there is not needs in external additional limitation, ie

struct MyViewRepresentable: UIViewRepresentable {
func makeUIView(context: Context) -> UIView {
return MyView(frame: .zero)
}
func updateUIView(_ uiView: UIView, context: Context) {}
}

will be exactly sized below having 400x250 frame

struct ContentView: View {
var body: some View {
MyViewRepresentable()
.frame(width: 400, height: 250, alignment: .center)
}
}

if it is not then internal MyView layout has defects.



Related Topics



Leave a reply



Submit