extension View { /// Presents a conditional modal overlay. `onDismiss` gets invoked when the /// sheet is dismissed. /// /// On most platforms sheets appear as form-style modals. On tvOS, sheets /// appear as full screen overlays (non-opaque). /// /// `onDismiss` isn't called when the sheet gets dismissed programmatically /// (i.e. by setting `isPresented` to `false`). /// /// `onDismiss` gets called *after* the sheet has been dismissed by the /// underlying UI framework, and *before* `isPresented` gets set to false. /// /// - Parameters: /// - isPresented: A binding controlling whether the sheet is presented. /// - onDismiss: An action to perform when the sheet is dismissed /// by the user. /// - content: The content of the sheet public func sheet( isPresented: Binding, onDismiss: (() -> Void)? = nil, @ViewBuilder content: @escaping () -> SheetContent ) -> some View { SheetModifier( isPresented: isPresented, body: TupleView1(self), onDismiss: onDismiss, sheetContent: content ) } } struct SheetModifier: TypeSafeView { typealias Children = SheetModifierViewChildren var isPresented: Binding var body: TupleView1 var onDismiss: (() -> Void)? var sheetContent: () -> SheetContent var sheet: Any? func children( backend: Backend, snapshots: [ViewGraphSnapshotter.NodeSnapshot]?, environment: EnvironmentValues ) -> Children { let bodyViewGraphNode = ViewGraphNode( for: body.view0, backend: backend, environment: environment ) let bodyNode = AnyViewGraphNode(bodyViewGraphNode) return SheetModifierViewChildren( childNode: bodyNode, sheetContentNode: nil, sheet: nil ) } func asWidget( _ children: Children, backend: Backend ) -> Backend.Widget { children.childNode.widget.into() } func computeLayout( _ widget: Backend.Widget, children: Children, proposedSize: ProposedViewSize, environment: EnvironmentValues, backend: Backend ) -> ViewLayoutResult { children.childNode.computeLayout( with: body.view0, proposedSize: proposedSize, environment: environment ) } func commit( _: Backend.Widget, children: Children, layout: ViewLayoutResult, environment: EnvironmentValues, backend: Backend ) { func castBackend_inner(_ backend: NewBackend) { _ = children.childNode.commit() if isPresented.wrappedValue { let needsPresenting = children.sheet == nil let sheet: NewBackend.Sheet if children.sheetContentNode == nil { let sheetViewGraphNode = ViewGraphNode( for: sheetContent(), backend: backend, environment: environment ) let sheetContentNode = AnyViewGraphNode(sheetViewGraphNode) children.sheetContentNode = sheetContentNode sheet = backend.createSheet( content: children.sheetContentNode!.widget.into() ) } else { guard let existingSheet = children.sheet, let castedSheet = existingSheet as? NewBackend.Sheet else { logger.warning( """ SheetModifier has a nil sheet, even though the sheet \ has already been presented """ ) return } sheet = castedSheet } let dismissAction = DismissAction(action: { [isPresented] in isPresented.wrappedValue = false }) let sheetEnvironment = environment .with(\.dismiss, dismissAction) .with(\.sheet, sheet) _ = children.sheetContentNode!.computeLayout( with: sheetContent(), proposedSize: .unspecified, environment: sheetEnvironment ) let result = children.sheetContentNode!.commit() let window = environment.window! let preferences = result.preferences backend.updateSheet( sheet, window: window as! NewBackend.Window, // We intentionally use the outer environment rather than // sheetEnvironment here, because this is meant to be the sheet's // environment, not that of its content. environment: environment, size: result.size.vector, onDismiss: { handleDismiss(children: children) }, cornerRadius: preferences.presentationCornerRadius, detents: preferences.presentationDetents ?? [], dragIndicatorVisibility: preferences.presentationDragIndicatorVisibility ?? .automatic, backgroundColor: preferences.presentationBackground?.resolve(in: environment), interactiveDismissDisabled: preferences.interactiveDismissDisabled ?? false ) let parentSheet = environment.sheet.map { $0 as! NewBackend.Sheet } if needsPresenting { backend.presentSheet( sheet, window: window as! NewBackend.Window, parentSheet: parentSheet ) } children.sheet = sheet children.window = window children.parentSheet = parentSheet } else if !isPresented.wrappedValue && children.sheet != nil { backend.dismissSheet( children.sheet as! NewBackend.Sheet, window: children.window! as! NewBackend.Window, parentSheet: children.parentSheet.map { $0 as! NewBackend.Sheet } ) children.sheet = nil children.window = nil children.parentSheet = nil children.sheetContentNode = nil } } guard let castedBackend = backend as? any (BaseAppBackend & BackendFeatures.Sheets) else { fatalError("Backend does not implement BackendFeatures.Sheets") } castBackend_inner(castedBackend) } func handleDismiss(children: Children) { onDismiss?() children.sheet = nil children.window = nil children.parentSheet = nil children.sheetContentNode = nil isPresented.wrappedValue = false } } class SheetModifierViewChildren: ViewGraphNodeChildren { var widgets: [AnyWidget] { [childNode.widget] } var erasedNodes: [ErasedViewGraphNode] { var nodes: [ErasedViewGraphNode] = [ErasedViewGraphNode(wrapping: childNode)] if let sheetContentNode { nodes.append(ErasedViewGraphNode(wrapping: sheetContentNode)) } return nodes } var childNode: AnyViewGraphNode var sheetContentNode: AnyViewGraphNode? var sheet: Any? var window: Any? var parentSheet: Any? init( childNode: AnyViewGraphNode, sheetContentNode: AnyViewGraphNode?, sheet: Any? ) { self.childNode = childNode self.sheetContentNode = sheetContentNode self.sheet = sheet } }