SwiftUI View not updating its state when embedded in UIView (using UIHostingController)
Asked Answered
C

2

22

I'm wanting to use a SwiftUI View as content for a child UIView (which in my app would be inside UIViewController) by passing SwiftUI. However the SwiftUI View doesn't respond to state changes once embedded inside UIView.

I created the simplified version of my code below that has the issue. When tapping the Text View embedded inside the EmbedSwiftUIView the outer Text View at the top VStack updates as expected but the Text View embedded inside the EmbedSwiftUIView does not update its state.

struct ProblemView: View {

    @State var count = 0

    var body: some View {
        VStack {
            Text("Count is: \(self.count)")
            EmbedSwiftUIView {
                Text("Tap to increase count: \(self.count)")
                    .onTapGesture {
                        self.count = self.count + 1
                }
            }
        }
    }
}

struct EmbedSwiftUIView<Content:View> : UIViewRepresentable {

    var content: () -> Content

    func makeUIView(context: UIViewRepresentableContext<EmbedSwiftUIView<Content>>) -> UIView {
        let host = UIHostingController(rootView: content())
        return host.view
    }

    func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<EmbedSwiftUIView<Content>>) {

    }
}
Chelonian answered 5/3, 2020 at 8:15 Comment(1)
Since UIHostingController is for embedding SwiftUI Views in the UIKit world, I wonder if I understand your question correctly. In case you mean something else, which is why does it not update using UIViewRepresentable – this could be the solution: Utitlize func updateUIView() to dig into your UIKit and find your UIKit element to update manually. I know you have a Text() inside your UIKit Representable. This however might be excluded from updating because of the SwiftUI diffing mechanisms, which might not attempt to look for SwiftUI embedded in UIKit. Hope you get my point.Einkorn
D
15

Update view or view controller in updateUIView or updateUIViewController function. In this case, using UIViewControllerRepresentable is easier.

struct EmbedSwiftUIView<Content: View> : UIViewControllerRepresentable {

    var content: () -> Content

    func makeUIViewController(context: Context) -> UIHostingController<Content> {
        UIHostingController(rootView: content())
    }

    func updateUIViewController(_ host: UIHostingController<Content>, context: Context) {
        host.rootView = content() // Update content
    }
}
Downe answered 25/5, 2020 at 18:16 Comment(2)
Awesome. I feared updating rootView would cause it to loose its state, but that's not the case! PerfectBoley
Performance wise it is disaster.Thrombophlebitis
T
2

The only way is to create dedicated view with encapsulated increment logic:

struct IncrementView: View {
    @Binding var count: Int

    var body: some View {
        Text("Tap to increase count: \(count)")
            .onTapGesture {
                count += 1
        }
    }
}

struct ProblemView: View {
    @State var count = 0

    var body: some View {
        VStack {
            Text("Count is: \(count)")
            EmbedSwiftUIView {
                IncrementView(count: $count)
            }
        }
    }
}
Thrombophlebitis answered 11/11, 2022 at 0:1 Comment(1)
This works perfectly. And it sort of forces you to make your code more modular. (Which may not that helpful if the subview was really trivial. But otherwise, this is a nice side benefit.)Westmorland

© 2022 - 2024 — McMap. All rights reserved.