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)
})
}
}
}
}