Reputation: 18765
A SwiftUI
view should show different .sheet
s. For example some EditorView
with a picker view in a .sheet
for a number of different values.
Instead of using different @State var
s and .sheet
blocks for any picker, I created the following Router
and RouterView
to handle the sheet presentation:
class Router: ObservableObject {
@Published var sheetContent: AnyView? = nil
func present<Content: View>(content: Content) {
self.sheetContent = AnyView(content)
}
}
struct RouterView<Content: View>: View {
@ObservedObject var router: Router
private var content: () -> Content
init (_ router: Router, @ViewBuilder content: @escaping () -> Content) {
self.router = router
self.content = content
}
var body: some View {
content()
.sheet(isPresented: Binding(
get: { router.sheetContent != nil },
set: { if !$0 { router.sheetContent = nil } }
)) {
router.sheetContent
}
}
}
While this works fine in general, it somehow breaks @Binding
s used in the presented content, like in the demo code below.
The demo includes:
RouterTestView
holding the viewModel
with its Router
instance. It shows the current value of viewModel.showTitle
and Buttons to show the sheet in three different ways:
ValueView
is displayed.title
is shown or hidden depends on the current value of $viewModel.showTitleRouterTestView
and within the ValueView
Router
sheets, toggling $viewModel.showTitle
is correctly applied to the viewModel. When closing the sheet RouterTestView
correctly shows true or false. When presenting another sheet the initial state (hidden/visible) of the title is correct.ValueView
. The title text does not change its visibility.Conclusion: Using the Router somehow breaks the Binding
it only works from ValueView
back to the viewModel
while the other direction (viewModel
to ValueView
) does not work.
How can this be solved?
Demo Code:
// View Model
extension RouterTestView {
class ViewModelModel: ObservableObject {
@Published var router = Router()
@Published var title: String = "The Value"
@Published var showTitle: Bool = true
func show() {
router.present(content: ValueView(value: title, showValue: Binding(get: { self.showTitle }, set: { self.showTitle = $0 })))
}
func show<Content: View>(_ inView: Content) {
router.present(content: inView)
}
}
}
struct RouterTestView: View {
@StateObject var viewModel: ViewModelModel = .init()
@State private var isPresentingSheet: Bool = false
var body: some View {
Text("showTitle == \(viewModel.showTitle ? "true" : "false")")
// Do not use Router but instead use the classic way to present
// the sheet. The Binding works without any problem.
Button("Present Sheet manually") {
isPresentingSheet = true
}
.padding()
.sheet(isPresented: $isPresentingSheet) {
ValueView(value: viewModel.title, showValue: $viewModel.showTitle)
}
// Use Router to present the sheet. It makes no difference if the
// Content is created in the ViewModel or here. The Binding is broken.
// Changes to $viewModel.showTitle in ValueView are correctly reported
// to the viewModel, but the changes are not recognized in ValueView
// itself
RouterView(viewModel.router) {
// Sheet with Content created in viewModel
Button("Present Sheet with Router") {
viewModel.show()
}
.padding()
// Sheet with Content created here.
Button("Present Sheet with Router") {
viewModel.show(
ValueView(value: viewModel.title, showValue: $viewModel.showTitle)
)
}
.padding()
}
}
}
// ValueView as dummy "editor". Changes in the showValue Binding should be reported
// Back to the viewModel in RouterTestView and toggle the visibility of the value here.
struct ValueView: View {
let value: String
@Binding var showValue: Bool
var body: some View {
Text("Here is the value:")
if showValue {
Text(value)
}
Button("Toggle") {
showValue.toggle()
}
}
}
#Preview {
RouterTestView()
}
Upvotes: -3
Views: 60
Reputation: 29614
No, this setup is not expected to work.
The simplest solution is to use .sheet(item:)
.
But things like AnyView
completely hide any underlying updates.
I suggest watching “Demystify SwiftUI” and looking at this old post.
SwiftUI is value based, not reference based like UIKit.
Upvotes: 1