List.swift 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. /// A view that displays a selectable list of views.
  2. public struct List<SelectionValue: Hashable, RowView: View>: TypeSafeView, View {
  3. typealias Children = ListViewChildren<PaddingModifierView<RowView>>
  4. public let body = EmptyView()
  5. /// The current selection, if any.
  6. var selection: Binding<SelectionValue?>
  7. var rowContent: (Int) -> RowView
  8. var associatedSelectionValue: (Int) -> SelectionValue
  9. var find: (SelectionValue) -> Int?
  10. var rowCount: Int
  11. /// Creates a list view.
  12. ///
  13. /// - Parameters:
  14. /// - data: A collection of `Identifiable` values to construct the list
  15. /// from.
  16. /// - selection: A binding to the ID of the value that is currently
  17. /// selected.
  18. /// - rowContent: A view builder that renders a single row of the list.
  19. /// Receives an element of `data`.
  20. public init<Data: RandomAccessCollection>(
  21. _ data: Data,
  22. selection: Binding<SelectionValue?>,
  23. @ViewBuilder rowContent: @escaping (Data.Element) -> RowView
  24. ) where Data.Element: Identifiable, Data.Element.ID == SelectionValue, Data.Index == Int {
  25. self.init(data, id: \.id, selection: selection, rowContent: rowContent)
  26. }
  27. /// Creates a list view that renders `Text` views based on the elements of
  28. /// `data`.
  29. ///
  30. /// - Parameters:
  31. /// - data: A collection of `Identifiable` values to construct the list
  32. /// from.
  33. /// - selection: A binding to the ID of the value that is currently
  34. /// selected.
  35. public init<Data: RandomAccessCollection>(
  36. _ data: Data,
  37. selection: Binding<SelectionValue?>
  38. )
  39. where
  40. Data.Element: CustomStringConvertible & Identifiable,
  41. Data.Element.ID == SelectionValue,
  42. Data.Index == Int,
  43. RowView == Text
  44. {
  45. self.init(data, selection: selection) { item in
  46. return Text(item.description)
  47. }
  48. }
  49. /// Creates a list view that renders `Text` views based on the elements of
  50. /// `data`.
  51. ///
  52. /// - Parameters:
  53. /// - data: A collection of values to construct the list from.
  54. /// - id: A closure that returns the ID to use for a given element of
  55. /// `data`.
  56. /// - selection: A binding to the ID of the value that is currently
  57. /// selected.
  58. public init<Data: RandomAccessCollection>(
  59. _ data: Data,
  60. id: @escaping (Data.Element) -> SelectionValue,
  61. selection: Binding<SelectionValue?>
  62. ) where Data.Element: CustomStringConvertible, RowView == Text, Data.Index == Int {
  63. self.init(data, id: id, selection: selection) { item in
  64. return Text(item.description)
  65. }
  66. }
  67. /// Creates a list view that renders `Text` views based on the elements of
  68. /// `data`.
  69. ///
  70. /// - Parameters:
  71. /// - data: A collection of values to construct the list from.
  72. /// - id: A key path to the ID to use for an element of `data`.
  73. /// - selection: A binding to the ID of the value that is currently
  74. /// selected.
  75. public init<Data: RandomAccessCollection>(
  76. _ data: Data,
  77. id: KeyPath<Data.Element, SelectionValue>,
  78. selection: Binding<SelectionValue?>
  79. ) where Data.Element: CustomStringConvertible, RowView == Text, Data.Index == Int {
  80. self.init(data, id: id, selection: selection) { item in
  81. return Text(item.description)
  82. }
  83. }
  84. /// Creates a list view.
  85. ///
  86. /// - Parameters:
  87. /// - data: A collection of values to construct the list from.
  88. /// - id: A key path to the ID to use for an element of `data`.
  89. /// - selection: A binding to the ID of the value that is currently
  90. /// selected.
  91. /// - rowContent: A view builder that renders a single row of the list.
  92. /// Receives an element of `data`.
  93. public init<Data: RandomAccessCollection>(
  94. _ data: Data,
  95. id: KeyPath<Data.Element, SelectionValue>,
  96. selection: Binding<SelectionValue?>,
  97. @ViewBuilder rowContent: @escaping (Data.Element) -> RowView
  98. ) where Data.Index == Int {
  99. self.init(data, id: { $0[keyPath: id] }, selection: selection, rowContent: rowContent)
  100. }
  101. /// Creates a list view.
  102. ///
  103. /// - Parameters:
  104. /// - data: A collection of values to construct the list from.
  105. /// - id: A closure that returns the ID to use for a given element of
  106. /// `data`.
  107. /// - selection: A binding to the ID of the value that is currently
  108. /// selected.
  109. /// - rowContent: A view builder that renders a single row of the list.
  110. /// Receives an element of `data`.
  111. public init<Data: RandomAccessCollection>(
  112. _ data: Data,
  113. id: @escaping (Data.Element) -> SelectionValue,
  114. selection: Binding<SelectionValue?>,
  115. @ViewBuilder rowContent: @escaping (Data.Element) -> RowView
  116. ) where Data.Index == Int {
  117. self.selection = selection
  118. self.rowContent = { index in
  119. rowContent(data[index])
  120. }
  121. associatedSelectionValue = { index in
  122. id(data[index])
  123. }
  124. find = { selection in
  125. data.firstIndex { item in
  126. id(item) == selection
  127. }
  128. }
  129. rowCount = data.count
  130. }
  131. func children<Backend: BaseAppBackend>(
  132. backend: Backend,
  133. snapshots: [ViewGraphSnapshotter.NodeSnapshot]?,
  134. environment: EnvironmentValues
  135. ) -> Children {
  136. // TODO: Implement snapshotting
  137. Children()
  138. }
  139. func asWidget<Backend: BaseAppBackend>(
  140. _ children: Children,
  141. backend: Backend
  142. ) -> Backend.Widget {
  143. backend.createSelectableListView()
  144. }
  145. func computeLayout<Backend: BaseAppBackend>(
  146. _ widget: Backend.Widget,
  147. children: Children,
  148. proposedSize: ProposedViewSize,
  149. environment: EnvironmentValues,
  150. backend: Backend
  151. ) -> ViewLayoutResult {
  152. // Padding that the backend could not remove (some frameworks have a small
  153. // constant amount of required padding within each row).
  154. let baseRowPadding = backend.baseItemPadding(ofSelectableListView: widget)
  155. let minimumRowSize = backend.minimumRowSize(ofSelectableListView: widget)
  156. let horizontalBasePadding = baseRowPadding.axisTotals.x
  157. let verticalBasePadding = baseRowPadding.axisTotals.y
  158. let rowViews = (0..<rowCount).map(rowContent).map { rowView in
  159. PaddingModifierView(
  160. body: TupleView1(rowView),
  161. insets: EdgeInsets.Internal(
  162. top: max(6 - baseRowPadding.top, 0),
  163. bottom: max(6 - baseRowPadding.bottom, 0),
  164. leading: max(8 - baseRowPadding.leading, 0),
  165. trailing: max(8 - baseRowPadding.trailing, 0)
  166. )
  167. )
  168. }
  169. if rowCount > children.nodes.count {
  170. for rowView in rowViews.dropFirst(children.nodes.count) {
  171. let node = AnyViewGraphNode(
  172. for: rowView,
  173. backend: backend,
  174. environment: environment
  175. )
  176. children.nodes.append(node)
  177. }
  178. } else if children.nodes.count > rowCount {
  179. children.nodes.removeLast(children.nodes.count - rowCount)
  180. }
  181. var childResults: [ViewLayoutResult] = []
  182. for (rowView, node) in zip(rowViews, children.nodes) {
  183. let proposedWidth: Double?
  184. if let width = proposedSize.width {
  185. proposedWidth = max(
  186. Double(minimumRowSize.x),
  187. width - Double(baseRowPadding.axisTotals.x)
  188. )
  189. } else {
  190. proposedWidth = nil
  191. }
  192. let childResult = node.computeLayout(
  193. with: rowView,
  194. proposedSize: ProposedViewSize(
  195. proposedWidth,
  196. nil
  197. ),
  198. environment: environment
  199. )
  200. childResults.append(childResult)
  201. }
  202. let height = childResults.map(\.size.height).map { rowHeight in
  203. max(
  204. rowHeight + Double(verticalBasePadding),
  205. Double(minimumRowSize.y)
  206. )
  207. }.reduce(0, +)
  208. let minimumWidth =
  209. (childResults.map(\.size.width).max() ?? 0) + Double(horizontalBasePadding)
  210. let size = ViewSize(
  211. max(proposedSize.width ?? minimumWidth, minimumWidth),
  212. height
  213. )
  214. return ViewLayoutResult(
  215. size: size,
  216. childResults: childResults
  217. )
  218. }
  219. func commit<Backend: BaseAppBackend>(
  220. _ widget: Backend.Widget,
  221. children: Children,
  222. layout: ViewLayoutResult,
  223. environment: EnvironmentValues,
  224. backend: Backend
  225. ) {
  226. let baseRowPadding = backend.baseItemPadding(ofSelectableListView: widget)
  227. let verticalBasePadding = baseRowPadding.axisTotals.y
  228. let childResults = children.nodes.map { $0.commit() }
  229. backend.setItems(
  230. ofSelectableListView: widget,
  231. to: children.widgets.map { $0.into() },
  232. withRowHeights: childResults.map(\.size.height).map { height in
  233. LayoutSystem.roundSize(height) + verticalBasePadding
  234. }
  235. )
  236. backend.setSize(of: widget, to: layout.size.vector)
  237. backend.setSelectionHandler(forSelectableListView: widget) { selectedIndex in
  238. selection.wrappedValue = associatedSelectionValue(selectedIndex)
  239. }
  240. let selectedIndex: Int?
  241. if let selectedItem = selection.wrappedValue {
  242. selectedIndex = find(selectedItem)
  243. } else {
  244. selectedIndex = nil
  245. }
  246. backend.updateSelectableListView(widget, environment: environment)
  247. backend.setSelectedItem(ofSelectableListView: widget, toItemAt: selectedIndex)
  248. }
  249. }
  250. class ListViewChildren<RowView: View>: ViewGraphNodeChildren {
  251. var nodes: [AnyViewGraphNode<RowView>]
  252. init() {
  253. nodes = []
  254. }
  255. var erasedNodes: [ErasedViewGraphNode] {
  256. nodes.map(ErasedViewGraphNode.init)
  257. }
  258. var widgets: [AnyWidget] {
  259. nodes.map(\.widget)
  260. }
  261. }