| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287 |
- /// A view that displays a selectable list of views.
- public struct List<SelectionValue: Hashable, RowView: View>: TypeSafeView, View {
- typealias Children = ListViewChildren<PaddingModifierView<RowView>>
- public let body = EmptyView()
- /// The current selection, if any.
- var selection: Binding<SelectionValue?>
- var rowContent: (Int) -> RowView
- var associatedSelectionValue: (Int) -> SelectionValue
- var find: (SelectionValue) -> Int?
- var rowCount: Int
- /// Creates a list view.
- ///
- /// - Parameters:
- /// - data: A collection of `Identifiable` values to construct the list
- /// from.
- /// - selection: A binding to the ID of the value that is currently
- /// selected.
- /// - rowContent: A view builder that renders a single row of the list.
- /// Receives an element of `data`.
- public init<Data: RandomAccessCollection>(
- _ data: Data,
- selection: Binding<SelectionValue?>,
- @ViewBuilder rowContent: @escaping (Data.Element) -> RowView
- ) where Data.Element: Identifiable, Data.Element.ID == SelectionValue, Data.Index == Int {
- self.init(data, id: \.id, selection: selection, rowContent: rowContent)
- }
- /// Creates a list view that renders `Text` views based on the elements of
- /// `data`.
- ///
- /// - Parameters:
- /// - data: A collection of `Identifiable` values to construct the list
- /// from.
- /// - selection: A binding to the ID of the value that is currently
- /// selected.
- public init<Data: RandomAccessCollection>(
- _ data: Data,
- selection: Binding<SelectionValue?>
- )
- where
- Data.Element: CustomStringConvertible & Identifiable,
- Data.Element.ID == SelectionValue,
- Data.Index == Int,
- RowView == Text
- {
- self.init(data, selection: selection) { item in
- return Text(item.description)
- }
- }
- /// Creates a list view that renders `Text` views based on the elements of
- /// `data`.
- ///
- /// - Parameters:
- /// - data: A collection of values to construct the list from.
- /// - id: A closure that returns the ID to use for a given element of
- /// `data`.
- /// - selection: A binding to the ID of the value that is currently
- /// selected.
- public init<Data: RandomAccessCollection>(
- _ data: Data,
- id: @escaping (Data.Element) -> SelectionValue,
- selection: Binding<SelectionValue?>
- ) where Data.Element: CustomStringConvertible, RowView == Text, Data.Index == Int {
- self.init(data, id: id, selection: selection) { item in
- return Text(item.description)
- }
- }
- /// Creates a list view that renders `Text` views based on the elements of
- /// `data`.
- ///
- /// - Parameters:
- /// - data: A collection of values to construct the list from.
- /// - id: A key path to the ID to use for an element of `data`.
- /// - selection: A binding to the ID of the value that is currently
- /// selected.
- public init<Data: RandomAccessCollection>(
- _ data: Data,
- id: KeyPath<Data.Element, SelectionValue>,
- selection: Binding<SelectionValue?>
- ) where Data.Element: CustomStringConvertible, RowView == Text, Data.Index == Int {
- self.init(data, id: id, selection: selection) { item in
- return Text(item.description)
- }
- }
- /// Creates a list view.
- ///
- /// - Parameters:
- /// - data: A collection of values to construct the list from.
- /// - id: A key path to the ID to use for an element of `data`.
- /// - selection: A binding to the ID of the value that is currently
- /// selected.
- /// - rowContent: A view builder that renders a single row of the list.
- /// Receives an element of `data`.
- public init<Data: RandomAccessCollection>(
- _ data: Data,
- id: KeyPath<Data.Element, SelectionValue>,
- selection: Binding<SelectionValue?>,
- @ViewBuilder rowContent: @escaping (Data.Element) -> RowView
- ) where Data.Index == Int {
- self.init(data, id: { $0[keyPath: id] }, selection: selection, rowContent: rowContent)
- }
- /// Creates a list view.
- ///
- /// - Parameters:
- /// - data: A collection of values to construct the list from.
- /// - id: A closure that returns the ID to use for a given element of
- /// `data`.
- /// - selection: A binding to the ID of the value that is currently
- /// selected.
- /// - rowContent: A view builder that renders a single row of the list.
- /// Receives an element of `data`.
- public init<Data: RandomAccessCollection>(
- _ data: Data,
- id: @escaping (Data.Element) -> SelectionValue,
- selection: Binding<SelectionValue?>,
- @ViewBuilder rowContent: @escaping (Data.Element) -> RowView
- ) where Data.Index == Int {
- self.selection = selection
- self.rowContent = { index in
- rowContent(data[index])
- }
- associatedSelectionValue = { index in
- id(data[index])
- }
- find = { selection in
- data.firstIndex { item in
- id(item) == selection
- }
- }
- rowCount = data.count
- }
- func children<Backend: BaseAppBackend>(
- backend: Backend,
- snapshots: [ViewGraphSnapshotter.NodeSnapshot]?,
- environment: EnvironmentValues
- ) -> Children {
- // TODO: Implement snapshotting
- Children()
- }
- func asWidget<Backend: BaseAppBackend>(
- _ children: Children,
- backend: Backend
- ) -> Backend.Widget {
- backend.createSelectableListView()
- }
- func computeLayout<Backend: BaseAppBackend>(
- _ widget: Backend.Widget,
- children: Children,
- proposedSize: ProposedViewSize,
- environment: EnvironmentValues,
- backend: Backend
- ) -> ViewLayoutResult {
- // Padding that the backend could not remove (some frameworks have a small
- // constant amount of required padding within each row).
- let baseRowPadding = backend.baseItemPadding(ofSelectableListView: widget)
- let minimumRowSize = backend.minimumRowSize(ofSelectableListView: widget)
- let horizontalBasePadding = baseRowPadding.axisTotals.x
- let verticalBasePadding = baseRowPadding.axisTotals.y
- let rowViews = (0..<rowCount).map(rowContent).map { rowView in
- PaddingModifierView(
- body: TupleView1(rowView),
- insets: EdgeInsets.Internal(
- top: max(6 - baseRowPadding.top, 0),
- bottom: max(6 - baseRowPadding.bottom, 0),
- leading: max(8 - baseRowPadding.leading, 0),
- trailing: max(8 - baseRowPadding.trailing, 0)
- )
- )
- }
- if rowCount > children.nodes.count {
- for rowView in rowViews.dropFirst(children.nodes.count) {
- let node = AnyViewGraphNode(
- for: rowView,
- backend: backend,
- environment: environment
- )
- children.nodes.append(node)
- }
- } else if children.nodes.count > rowCount {
- children.nodes.removeLast(children.nodes.count - rowCount)
- }
- var childResults: [ViewLayoutResult] = []
- for (rowView, node) in zip(rowViews, children.nodes) {
- let proposedWidth: Double?
- if let width = proposedSize.width {
- proposedWidth = max(
- Double(minimumRowSize.x),
- width - Double(baseRowPadding.axisTotals.x)
- )
- } else {
- proposedWidth = nil
- }
- let childResult = node.computeLayout(
- with: rowView,
- proposedSize: ProposedViewSize(
- proposedWidth,
- nil
- ),
- environment: environment
- )
- childResults.append(childResult)
- }
- let height = childResults.map(\.size.height).map { rowHeight in
- max(
- rowHeight + Double(verticalBasePadding),
- Double(minimumRowSize.y)
- )
- }.reduce(0, +)
- let minimumWidth =
- (childResults.map(\.size.width).max() ?? 0) + Double(horizontalBasePadding)
- let size = ViewSize(
- max(proposedSize.width ?? minimumWidth, minimumWidth),
- height
- )
- return ViewLayoutResult(
- size: size,
- childResults: childResults
- )
- }
- func commit<Backend: BaseAppBackend>(
- _ widget: Backend.Widget,
- children: Children,
- layout: ViewLayoutResult,
- environment: EnvironmentValues,
- backend: Backend
- ) {
- let baseRowPadding = backend.baseItemPadding(ofSelectableListView: widget)
- let verticalBasePadding = baseRowPadding.axisTotals.y
- let childResults = children.nodes.map { $0.commit() }
- backend.setItems(
- ofSelectableListView: widget,
- to: children.widgets.map { $0.into() },
- withRowHeights: childResults.map(\.size.height).map { height in
- LayoutSystem.roundSize(height) + verticalBasePadding
- }
- )
- backend.setSize(of: widget, to: layout.size.vector)
- backend.setSelectionHandler(forSelectableListView: widget) { selectedIndex in
- selection.wrappedValue = associatedSelectionValue(selectedIndex)
- }
- let selectedIndex: Int?
- if let selectedItem = selection.wrappedValue {
- selectedIndex = find(selectedItem)
- } else {
- selectedIndex = nil
- }
- backend.updateSelectableListView(widget, environment: environment)
- backend.setSelectedItem(ofSelectableListView: widget, toItemAt: selectedIndex)
- }
- }
- class ListViewChildren<RowView: View>: ViewGraphNodeChildren {
- var nodes: [AnyViewGraphNode<RowView>]
- init() {
- nodes = []
- }
- var erasedNodes: [ErasedViewGraphNode] {
- nodes.map(ErasedViewGraphNode.init)
- }
- var widgets: [AnyWidget] {
- nodes.map(\.widget)
- }
- }
|