swiftswiftuiuiviewrepresentableuihostingcontroller

UIHostingController size too big


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.


Solution

  • 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