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()
}
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 MyView
s, 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.