Use Case
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 dismissedIf 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 this normal behavior?
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.
Visual
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):
Sample Project
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