UIHostingController size too big
Asked Answered
R

1

0

I'm trying to embed a SwiftUI View within a UIKit UIView, within a View again. It will look something like this:

View
↓
UIView
↓
View

Current code:

struct ContentView: View {
    var body: some View {
        Representable {
            Text("Hello world!")
        }
    }
}


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

I want the Representable to only set the backgroundColor of the Text. It shouldn't be any bigger. Also, this is just an example, so this isn't just a Text and setting the background color.

Now Aim
Now Aim

There is also a problem if the text is really long - it doesn't get constrained by the size of the screen / parent (using hugging priority in this case):

Long text

How can I make sure that Representable is only as big as the content itself, Text in this case? It should also work if the text wraps over a line for example when constrained to a certain width.

Report answered 15/2, 2022 at 13:37 Comment(2)
Does this answer your question https://mcmap.net/q/510949/-uiviewrepresentable-automatic-size-passing-uikit-uiview-size-to-swiftui? Also this can be helpful https://mcmap.net/q/1006285/-how-does-uiviewrepresentable-size-itself-in-relation-to-the-uikit-control-inside-it.Towill
@Towill The issue caused by all of this in general is the problem of the text being too long. I've added an image to the question to showReport
R
-1

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

Report answered 16/2, 2022 at 2:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.