SheetModifier.swift 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. extension View {
  2. /// Presents a conditional modal overlay. `onDismiss` gets invoked when the
  3. /// sheet is dismissed.
  4. ///
  5. /// On most platforms sheets appear as form-style modals. On tvOS, sheets
  6. /// appear as full screen overlays (non-opaque).
  7. ///
  8. /// `onDismiss` isn't called when the sheet gets dismissed programmatically
  9. /// (i.e. by setting `isPresented` to `false`).
  10. ///
  11. /// `onDismiss` gets called *after* the sheet has been dismissed by the
  12. /// underlying UI framework, and *before* `isPresented` gets set to false.
  13. ///
  14. /// - Parameters:
  15. /// - isPresented: A binding controlling whether the sheet is presented.
  16. /// - onDismiss: An action to perform when the sheet is dismissed
  17. /// by the user.
  18. /// - content: The content of the sheet
  19. public func sheet<SheetContent: View>(
  20. isPresented: Binding<Bool>,
  21. onDismiss: (() -> Void)? = nil,
  22. @ViewBuilder content: @escaping () -> SheetContent
  23. ) -> some View {
  24. SheetModifier(
  25. isPresented: isPresented,
  26. body: TupleView1(self),
  27. onDismiss: onDismiss,
  28. sheetContent: content
  29. )
  30. }
  31. }
  32. struct SheetModifier<Content: View, SheetContent: View>: TypeSafeView {
  33. typealias Children = SheetModifierViewChildren<Content, SheetContent>
  34. var isPresented: Binding<Bool>
  35. var body: TupleView1<Content>
  36. var onDismiss: (() -> Void)?
  37. var sheetContent: () -> SheetContent
  38. var sheet: Any?
  39. func children<Backend: BaseAppBackend>(
  40. backend: Backend,
  41. snapshots: [ViewGraphSnapshotter.NodeSnapshot]?,
  42. environment: EnvironmentValues
  43. ) -> Children {
  44. let bodyViewGraphNode = ViewGraphNode(
  45. for: body.view0,
  46. backend: backend,
  47. environment: environment
  48. )
  49. let bodyNode = AnyViewGraphNode(bodyViewGraphNode)
  50. return SheetModifierViewChildren(
  51. childNode: bodyNode,
  52. sheetContentNode: nil,
  53. sheet: nil
  54. )
  55. }
  56. func asWidget<Backend: BaseAppBackend>(
  57. _ children: Children,
  58. backend: Backend
  59. ) -> Backend.Widget {
  60. children.childNode.widget.into()
  61. }
  62. func computeLayout<Backend: BaseAppBackend>(
  63. _ widget: Backend.Widget,
  64. children: Children,
  65. proposedSize: ProposedViewSize,
  66. environment: EnvironmentValues,
  67. backend: Backend
  68. ) -> ViewLayoutResult {
  69. children.childNode.computeLayout(
  70. with: body.view0,
  71. proposedSize: proposedSize,
  72. environment: environment
  73. )
  74. }
  75. func commit<Backend: BaseAppBackend>(
  76. _: Backend.Widget,
  77. children: Children,
  78. layout: ViewLayoutResult,
  79. environment: EnvironmentValues,
  80. backend: Backend
  81. ) {
  82. func castBackend_inner<NewBackend: BaseAppBackend & BackendFeatures.Sheets>(_ backend: NewBackend) {
  83. _ = children.childNode.commit()
  84. if isPresented.wrappedValue {
  85. let needsPresenting = children.sheet == nil
  86. let sheet: NewBackend.Sheet
  87. if children.sheetContentNode == nil {
  88. let sheetViewGraphNode = ViewGraphNode(
  89. for: sheetContent(),
  90. backend: backend,
  91. environment: environment
  92. )
  93. let sheetContentNode = AnyViewGraphNode(sheetViewGraphNode)
  94. children.sheetContentNode = sheetContentNode
  95. sheet = backend.createSheet(
  96. content: children.sheetContentNode!.widget.into()
  97. )
  98. } else {
  99. guard
  100. let existingSheet = children.sheet,
  101. let castedSheet = existingSheet as? NewBackend.Sheet
  102. else {
  103. logger.warning(
  104. """
  105. SheetModifier has a nil sheet, even though the sheet \
  106. has already been presented
  107. """
  108. )
  109. return
  110. }
  111. sheet = castedSheet
  112. }
  113. let dismissAction = DismissAction(action: { [isPresented] in
  114. isPresented.wrappedValue = false
  115. })
  116. let sheetEnvironment =
  117. environment
  118. .with(\.dismiss, dismissAction)
  119. .with(\.sheet, sheet)
  120. _ = children.sheetContentNode!.computeLayout(
  121. with: sheetContent(),
  122. proposedSize: .unspecified,
  123. environment: sheetEnvironment
  124. )
  125. let result = children.sheetContentNode!.commit()
  126. let window = environment.window!
  127. let preferences = result.preferences
  128. backend.updateSheet(
  129. sheet,
  130. window: window as! NewBackend.Window,
  131. // We intentionally use the outer environment rather than
  132. // sheetEnvironment here, because this is meant to be the sheet's
  133. // environment, not that of its content.
  134. environment: environment,
  135. size: result.size.vector,
  136. onDismiss: { handleDismiss(children: children) },
  137. cornerRadius: preferences.presentationCornerRadius,
  138. detents: preferences.presentationDetents ?? [],
  139. dragIndicatorVisibility: preferences.presentationDragIndicatorVisibility
  140. ?? .automatic,
  141. backgroundColor: preferences.presentationBackground?.resolve(in: environment),
  142. interactiveDismissDisabled: preferences.interactiveDismissDisabled ?? false
  143. )
  144. let parentSheet = environment.sheet.map { $0 as! NewBackend.Sheet }
  145. if needsPresenting {
  146. backend.presentSheet(
  147. sheet,
  148. window: window as! NewBackend.Window,
  149. parentSheet: parentSheet
  150. )
  151. }
  152. children.sheet = sheet
  153. children.window = window
  154. children.parentSheet = parentSheet
  155. } else if !isPresented.wrappedValue && children.sheet != nil {
  156. backend.dismissSheet(
  157. children.sheet as! NewBackend.Sheet,
  158. window: children.window! as! NewBackend.Window,
  159. parentSheet: children.parentSheet.map { $0 as! NewBackend.Sheet }
  160. )
  161. children.sheet = nil
  162. children.window = nil
  163. children.parentSheet = nil
  164. children.sheetContentNode = nil
  165. }
  166. }
  167. guard let castedBackend = backend as? any (BaseAppBackend & BackendFeatures.Sheets) else {
  168. fatalError("Backend does not implement BackendFeatures.Sheets")
  169. }
  170. castBackend_inner(castedBackend)
  171. }
  172. func handleDismiss(children: Children) {
  173. onDismiss?()
  174. children.sheet = nil
  175. children.window = nil
  176. children.parentSheet = nil
  177. children.sheetContentNode = nil
  178. isPresented.wrappedValue = false
  179. }
  180. }
  181. class SheetModifierViewChildren<Child: View, SheetContent: View>: ViewGraphNodeChildren {
  182. var widgets: [AnyWidget] {
  183. [childNode.widget]
  184. }
  185. var erasedNodes: [ErasedViewGraphNode] {
  186. var nodes: [ErasedViewGraphNode] = [ErasedViewGraphNode(wrapping: childNode)]
  187. if let sheetContentNode {
  188. nodes.append(ErasedViewGraphNode(wrapping: sheetContentNode))
  189. }
  190. return nodes
  191. }
  192. var childNode: AnyViewGraphNode<Child>
  193. var sheetContentNode: AnyViewGraphNode<SheetContent>?
  194. var sheet: Any?
  195. var window: Any?
  196. var parentSheet: Any?
  197. init(
  198. childNode: AnyViewGraphNode<Child>,
  199. sheetContentNode: AnyViewGraphNode<SheetContent>?,
  200. sheet: Any?
  201. ) {
  202. self.childNode = childNode
  203. self.sheetContentNode = sheetContentNode
  204. self.sheet = sheet
  205. }
  206. }