swiftmacosswiftuiobservation

How to Extract View with an Observable Object?


I am writing a simple application (MacOS) to demonstrate to myself how to use Observable Objects. The application will also make the use of .sheets

The Observation is used in an external model.swift file.

@Observable
class ApplicationData {
    var isShowingSheet = false
}

In the ContentView I have placed a Button, which will toggle isShowingSheet when the button is pressed.

When the application is Run, the app does what it is meant to do, display a sheet when a button is pressed:

struct ContentView: View {
    @Bindable var appData = ApplicationData()
    
    var body: some View {
            Button ("Show more information.") {
                appData.isShowingSheet.toggle()
            }.sheet(isPresented: $appData.isShowingSheet, onDismiss: didDismiss) {
                VStack{
                    Text("Brewing Database")
                        .font(.title)
                        .padding(32)
                    
                    Text("Showing sheet")
                        .padding(16)
                    
                    Button("Dismiss", action: {appData.isShowingSheet.toggle()})
                }
                
            }
    }
}

#Preview {
    ContentView()
}

func didDismiss() {
    print("User dismissed the sheet.")
}

Now I would like to place the .sheet into an Extracted SubView like shown:

struct ContentView: View {
    @Bindable var appData = ApplicationData()
    
    var body: some View {
            Button ("Show more information.") {
                appData.isShowingSheet.toggle()
            }.sheet(isPresented: $appData.isShowingSheet, onDismiss: didDismiss) {
                ExtractedView()   
            }
    }
}

#Preview {
    ContentView()
}

func didDismiss() {
    print("User dismissed the sheet.")
}

struct ExtractedView: View {
    @Bindable var appData = ApplicationData()
    
    var body: some View {
        VStack{
            Text("Brewing Database")
                .font(.title)
                .padding(32)
            
            Text("Showing sheet")
                .padding(16)
            
            Button("Dismiss", action: {appData.isShowingSheet.toggle()})
        }
    }
}

It displays the sheet, but when pressing the Dismiss button, it will not dismiss.

Why is this so?


Solution

  • Since ContentView owns the instance of ApplicationData, you should use @State, not @Bindable.

    struct ContentView: View {
        @State var appData = ApplicationData()
        
        var body: some View {
            ...
        }
    }
    

    This instance of appData should be passed to ExtractedView. ExtractedView should not create its own instance.

    struct ExtractedView: View {
        // this could be '@Bindable var appData: ApplicationData' if you need a Binding 
        // to one of the properties, but you don't in this case
        let appData: ApplicationData
        
        var body: some View {
            VStack{
                Text("Brewing Database")
                    .font(.title)
                    .padding(32)
                
                Text("Showing sheet")
                    .padding(16)
                
                Button("Dismiss", action: {appData.isShowingSheet.toggle()})
            }
        }
    }
    
    .sheet(isPresented: $appData.isShowingSheet, onDismiss: didDismiss) {
        ExtractedView(appData: appData)   
    }
    

    That said, you don't need the ApplicationData instance in order to dismiss a sheet. Use the dismiss environment value instead.

    struct ExtractedView: View {
        @Environment(\.dismiss) var dismiss
        
        var body: some View {
            VStack{
                Text("Brewing Database")
                    .font(.title)
                    .padding(32)
                
                Text("Showing sheet")
                    .padding(16)
                
                Button("Dismiss", action: { dismiss() })
            } 
        }
    }