Body of a SwiftUI .sheet() is being evaluated after dismiss and not presenting

1k Views Asked by At

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

  2. 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 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):

Body-redrawn-without-Binding

2 - Body is NOT evaluated when using a @Binding (see lines 27-28 and 53-54):

Body-NOT-redrawn-with-Binding

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

1

There are 1 best solutions below

0
On

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