View nested in ObservableObject cast as AnyView not redrawing

Hello there! I've built a custom Modal component in SwiftUI that can be placed in a parent component and have dynamic content triggered by any child components. The full code for it is below.

The only problem I'm encountering is when the modal content has state. The view does not seem to be redrawing when bindings change. I believe it may be due to the AnyView casting, however I'm not sure how else I can store a SwiftUI view in an ObservableObject.

Any help is greatly appreciated!


// Controls the modal view
class ModalController: ObservableObject {
    @Published var visible: Bool
    @Published var content: AnyView
    
    var visibleBinding: Binding<Bool> {
        .init(get: { self.visible }, set: { self.visible = $0 })
    }

    init() {
        self.visible = false
        self.content = AnyView(EmptyView())
    }
    
    func present<C: View>(@ViewBuilder content: @escaping () -> C) {
        self.content = AnyView(content())
        self.visible = true
    }
    
    func hide() {
        self.visible = false
    }
}


// The modal view
struct Modal<Content: View>: View {
    @Binding var visible: Bool
    
    var content: Content
    
    init(visible: Binding<Bool>, @ViewBuilder content: () -> Content) {
        self.content = content()
        self._visible = visible
    }
    
    var body: some View {
            ZStack {
                Color.black.opacity(0.15)
                    .onTapGesture {
                        goBack()
                    }
                
                content
                    .padding()
                    .background {
                        RoundedRectangle(cornerRadius: 16, style: .continuous).fill(Color.white).shadow(color: .black.opacity(0.2), radius: 4, x: 0, y: 2)
                    }
                    .padding()
                    .padding()
            }
            .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
            .ignoresSafeArea(.all)
            .visible(visible)
            .animation(.interactiveSpring(), value: visible)
    }
    
    
    func goBack() {
        visible = false
    }
}


// View extension
extension View {
    func modal<C: View>(visible: Binding<Bool>, @ViewBuilder content: @escaping () -> C) -> some View {
        ZStack {
            self
            Modal(visible: visible, content: content)
        }
    }
}



// Using the modal 
struct ParentView: View {
    @StateObject private var modalController: ModalController = ModalController()

    var body: some View {
        VStack  {
            
            ...

            VStack {
               ...
            }
            .modal(visible: modalController.visibleBinding) {
                modalController.content
            }
        }
        .environmentObject(modalController)
    }
}

struct SomeChild: View {
    @EnvironmentObject var modalController: ModalController

    @Binding var foo: String

    var body: some View {
        VStack {
            ...
            Button("Open Modal") {
                modalController.present(content: {

                    // *** Problem here ***
                    // Modifying the binding works, but does not update the UI
                    AnotherChild(binding: $foo)
                })
            }
        }
    }
}

ObservableObject is designed to store model data, not a good idea to store a SwiftUI View in one or any transient view data at all for that matter. Also it's not a good idea to hang on to a SwiftUI View anywhere because SwiftUI expects to recreate all the View structs when the state they depend on changes.

Take a look at the EditorConfig sample code in Data Essentials in SwiftUI WWDC 2020, it explains why using value types make it work so should help you manage your data for showing in a modal/sheet.

Had a feeling this was no good, but it was working thus far. Will take a look at what you mentioned. Thanks.

1 Like