iosswiftswiftuibinding

Why does .fullScreenCover(isPresented: $someBinding) requires a binding instead of a simple bool?


I wonder why .fullScreenCover(isPresented:) requires a Binding instead of a simple Bool. I understand that .sheet requires a binding report back that it was closed, e.g. through a swipe gesture.

.fullScreenCover on the other hand, does not offer any build in methods to close the cover, does it? So the neither the user nor iOS can close the cover. So there is nothing to report back and thus a simple Bool parameter would be sufficient like in this example:

struct CoverTestView: View {
    @State var visible: Bool = false
    
    var body: some View {
        // Pass simple Bool instead of Binding
        CoverView(visible: visible)
        
        Button("Toggle") {
            visible.toggle()
        }
    }
}

struct CoverView: View {
    let visible: Bool
    
    var body: some View {
        if visible {
            Text("Hello, World!")
        }
    }
}

What is the Binding needed for?

Of course the user needs some way to close the cover and thus a binding is required. But it would be required in the cover content, not in .fullScreenCover itself.

struct CoverTestView: View {
    @State var visible: Bool = false
    
    var body: some View {
        Button("Toggle") {
            visible.toggle()
        }
        .fullScreenCover(isPresented: visible) {
            CoverView(visible: $visible)
        }
    }
}

struct CoverView: View {
    @Binding var visible: Bool
    
    var body: some View {
        if visible {
            Text("Hello, World!")
        }
        Button("Dismiss") {
            visible = false
        }
    }
}

I assume that the iOS engineers used the Binding for a reason. So what am I missing?


Solution

  • But it would be required in the cover content, not in .fullScreenCover itself.

    Sure, SwiftUI could be designed that way, but it is not. In the presented view, you are allowed to use the dismiss environment value to dismiss a fullscreen cover.

    struct CoverView: View {
        @Environment(\.dismiss) var dismiss
        
        var body: some View {
            Text("Hello, World!")
            Button("Dismiss") {
                dismiss()
            }
        }
    }
    

    Because of this, fullScreenCover needs to be able to change the value of isPresented too, and so it needs to be a Binding.

    This design also reduces the number of parameters you'd need to pass to your views. The views themselves can get the dismiss environment value all on their own, without having an extra initialiser parameter.

    Furthermore, if the view you are presenting is some UIViewController that uses UIViewController.dismiss to dismiss itself, isPresented will still be set to false afterwards, hence maintaing the source of truth.

    struct UIKitStuff: UIViewControllerRepresentable {
        class SomeController: UIViewController {
            override func motionBegan(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
                // this also sets the binding back to false!
                dismiss(animated: true)
            }
        }
        
        func makeUIViewController(context: Context) -> some UIViewController {
            SomeController()
        }
        
        func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
            
        }
    }
    

    If isPresented is not a Binding, then UIViewController.dismiss will either need to do nothing (which reduces the reusability of existing UIViewControllers), or the SwiftUI state will no longer reflect reality (visible being true despite there is no fullscreen cover). I'm sure you'd agree that neither is desirable.