How to force to create a new UIViewControllerRepresentable | updating views on makeUIViewController

141 views Asked by At

I have a problem with creating a new UIViewControllerRepresentable, as the makeUIViewController method runs only once in UIViewControllerRepresentable, preventing updates to the new view. What would be the optimal way to modify this code while maintaining the privacy of MyView within ControllerView?

private struct ControllerView<Content: View>: View {
    struct MyView<ContentM: View>: UIViewControllerRepresentable {
        let rootView: ContentM
        
        init(rootView: ContentM) {
            self.rootView = rootView
            print("init MyView")
        }
        
        func makeUIViewController(context: Context) -> UIViewController {
            print("makeUI")
            /// create my custom VC
            return UIHostingController(rootView: rootView)
        }
        func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
            print("updateUI")
        }
    }
    
    
    let rootView: () -> Content
    @Binding var isGreen: Bool
    
    init(isGreen: Binding<Bool>,@ViewBuilder rootView: @escaping () -> Content) {
        self.rootView = rootView
        self._isGreen = isGreen
    }
    
    var body: some View {
        ZStack {
            MyView(rootView: rootView())
            
            Button {
                isGreen.toggle()
            } label: {
                Text("Change")
            }
        }
    }
}

private struct GreenView: View  {
    var body: some View {Color.green.ignoresSafeArea()}
}
private struct OrangeView: View  {
    var body: some View {Color.orange.ignoresSafeArea()}
}




struct SwiftUIView21: View {
    @State var isGreen: Bool = true
    
    var body: some View {
        ControllerView(isGreen: $isGreen) {
            if isGreen {
                GreenView()
            } else {
                OrangeView()
            }
        }
    }
}

#Preview {
    SwiftUIView21()
}

1

There are 1 answers

4
Sweeper On BEST ANSWER

While makeUIViewController is not called, updateUIViewController is. You can change the view that is displayed in the UIHostingController there.

func makeUIViewController(context: Context) -> UIHostingController<ContentM> {
    print("makeUI")
    return UIHostingController(rootView: rootView)
}
func updateUIViewController(_ uiViewController: UIHostingController<ContentM>, context: Context) {
    uiViewController.rootView = rootView
    print("updateUI")
}

Note that this will break any animations you want to do when you toggle between the two views. SwiftUI can't animate uiViewController.rootView = rootView.


Alternatively, you can change MyView's id every time a toggle happens:

MyView(rootView: rootView()).id(isGreen)

This recreate a new UIHostingController every time you change views, and can animate the change. However, this only animates the change between two MyViews, which just so happens to look similar to the animation of changing between OrangeView and GreenView (both a cross-fade). If you have other animations like animating the scale of something:

Text("Foo").scaleEffect(isGreen ? 1 : 2)

Then the animation will still be a cross-fade, not a scale animation.


A third way I found was to use an @Observable wrapper to wrap the Bool, then put it in the Environment.

@Observable
class BoolWrapper: ExpressibleByBooleanLiteral {
    var bool: Bool
    required init(booleanLiteral value: Bool) {
        bool = value
    }
}

private struct ControllerView<Content: View>: View {
    struct MyView<ContentM: View>: UIViewControllerRepresentable {
        let rootView: () -> ContentM
        
        init(@ViewBuilder rootView: @escaping () -> ContentM) {
            self.rootView = rootView
            print("init MyView")
        }
        
        func makeUIViewController(context: Context) -> UIHostingController<ContentM> {
            print("makeUI")
            return UIHostingController(rootView: rootView())
        }
        func updateUIViewController(_ uiViewController: UIHostingController<ContentM>, context: Context) {
            print("updateUI")
        }
    }
    
    
    let rootView: () -> Content
    let isGreen: BoolWrapper
    
    init(isGreen: BoolWrapper,@ViewBuilder rootView: @escaping () -> Content) {
        self.rootView = rootView
        self.isGreen = isGreen
    }
    
    var body: some View {
        ZStack {
            MyView(rootView: rootView)
            
            Button {
                withAnimation {
                    isGreen.bool.toggle()
                }
            } label: {
                Text("Change")
            }
        }
    }
}

struct ContentView: View {
    @State var isGreen: BoolWrapper = true
    
    var body: some View {
        ControllerView(isGreen: isGreen) {
            GreenOrangeWrapper().environment(isGreen)
        }
    }
}

struct GreenOrangeWrapper: View {
    @Environment(BoolWrapper.self) var boolWrapper
    
    var body: some View {
        if boolWrapper.bool {
            GreenView()
        } else {
            OrangeView()
        }
    }
}

Now the animations are all preserved.