clns
clns

Reputation: 2324

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

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

Upvotes: 2

Views: 1234

Answers (2)

Marc
Marc

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

user14073836
user14073836

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

Related Questions