joshb
(Josh)
1
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)
})
}
}
}
}
malhal
(Malcolm Hall)
2
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.
joshb
(Josh)
3
Had a feeling this was no good, but it was working thus far. Will take a look at what you mentioned. Thanks.
1 Like