Reputation: 2324
If you have a SwiftUI ContentView()
that displays a PausableView()
based on a @State property presentSheet
that is also used to present a .sheet(), the body gets evaluated differently based on how the presentSheet
property is used inside the ContentView's body.
struct ContentView: View {
@State private var presentSheet = false
var body: some View {
return VStack{
Button("Show Sheet") {
presentSheet.toggle()
}
PausableView(isPaused: presentSheet) // 1. passing the property as a normal variable
// evaluates the body and the sheet on dismiss
// PausableView(isPaused: $presentSheet) // 2. passing the property as a @Binding
// doesn't evaluate the body when it changes on dismiss
}
.sheet(isPresented: $presentSheet) {
DismissingView(isPresented: $presentSheet)
}
}
}
If the property is sent to the PausableView(isPaused: presentSheet)
as a normal property, the body of the ContentView and the body of the sheet are being evaluated when the sheet is dismissed
If the property is sent to the PausableView(isPaused: $presentSheet)
as a @Binding, the body of the ContentView and the body of the sheet are NOT evaluated when the sheet is dismissed
Is the sheet's body supposed to be evaluated when the sheet is not presenting anymore after dismiss?
Also, using @Binding instead seems to change completely how the view body is evaluated. But sending it as a @Binding is not correct because the property should be read-only in the child view.
1 - Body gets evaluated when using a normal property (see lines 27-28 and 53-54):
2 - Body is NOT evaluated when using a @Binding (see lines 27-28 and 53-54):
A sample project created in Xcode 13 is available here: https://github.com/clns/SwiftUI-sheet-redraw-on-dismiss. I noticed the same behavior on iOS 14 and iOS 15.
The relevant code is in ContentView.swift:
import SwiftUI
struct DismissingView: View {
@Binding var isPresented: Bool
var body: some View {
if #available(iOS 15.0, *) {
print(Self._printChanges())
} else {
print("DismissingView: body draw")
}
return VStack {
Button("Dismiss") { isPresented.toggle() }
Text("Dismissing Sheet").padding()
}.background(Color.white)
}
}
struct PausableView: View {
var isPaused: Bool
// @Binding var isPaused: Bool
private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
@State private var counter = 0
var body: some View {
Text("Elapsed seconds: \(counter)")
.onReceive(timer) { _ in
counter += isPaused ? 0 : 1
}
}
}
struct ContentView: View {
@State private var presentSheet = false
var body: some View {
if #available(iOS 15.0, *) {
print(Self._printChanges())
} else {
print("ContentView: body draw")
}
return VStack{
Button("Show Sheet") { presentSheet.toggle() }
Text("The ContentView's body along with the .sheet() is being redrawn immediately after dismiss, if the @State property `presentSheet` is used anywhere else in the view - e.g. passed to `PausableView(isPaused:presentSheet)`.\n\nBut if the property is passed as a @Binding to `PausableView(isPaused:$presentSheet)`, the ContentView's body is not redrawn.").padding()
PausableView(isPaused: presentSheet)
// PausableView(isPaused: $presentSheet)
}
.sheet(isPresented: $presentSheet) {
DismissingView(isPresented: $presentSheet)
.background(BackgroundClearView()) // to see what's happening under the sheet
}
}
}
Posted on Apple Developer Forums too: https://developer.apple.com/forums/thread/691783
Upvotes: 2
Views: 1234
Reputation: 95
I have the same problem with .sheet(item:)
After dismissing the sheet, the URLSheet view gets evaluated again (even though it is not shown anymore), and catastrophically for me its .task
is run a second time, thus trying to open the URL a second time...
@State private var urlToOpen: URL? = nil
MainContent(urlToOpen: $urlToOpen) // 1
.sheet(item: $urlToOpen, onDismiss: sheetDismissed) { url in
let sheet = URLSheet(urlToOpen: url)
Sheet(sheetView: AnyView(sheet))
}
.onOpenURL { url in
urlToOpen = url // raise sheet
}
I tried to pass urlToOpen
as binding to MainContent, see // 1
.
But sadly your solution doesn't work for .sheet(item:)
Upvotes: 0
Reputation:
Using @Binding nets correct behaviour.
Body being evaluated when using a normal property seems to be a bug, because structs are supposed to be immutable... unless you use property wrappers like @State/@Binding/etc.
Also, using @Binding instead seems to change completely how the view body is evaluated. But sending it as a @Binding is not correct because the property should be read-only in the child view. Why do you think it's not correct? Using @Binding means that you read and modify the data you pass into your child view but your child view does not 'own' it.
/Xcode 12 user here
Upvotes: 0