Skip to content

Lists Tables and Data

Daniel S edited this page Jul 2, 2026 · 1 revision

Lists, Tables, and Data

DesignFoundation ships seven data-display components: DFList, DFListRow, DFTable, DFDataTable, DFDataGrid, DFProgressBar, and DFSkeleton. This page documents each, including their initializers, style protocols, theming, accessibility behavior, and a component-selection guide at the end.

Related pages: Getting-Started · Theming · Layout · Primitives


DFList

DFList is a scrollable, themed list container with optional swipe-to-delete and drag-to-reorder support. It wraps SwiftUI's List with DF theming applied to selection tinting, separator color, and background.

Type Constraint

Element must conform to Identifiable & Sendable.

Initializer

public struct DFList<Element: Identifiable & Sendable, RowContent: View>: View {
    public init(
        _ data:         [Element],
        selection:      Binding<Element.ID?>? = nil,
        onDelete:       ((IndexSet) -> Void)? = nil,
        onMove:         ((IndexSet, Int) -> Void)? = nil,
        @ViewBuilder row: @escaping (Element) -> RowContent
    )
}
Parameter Type Default Description
data [Element] required The items to display.
selection Binding<Element.ID?>? nil Optional selection binding. When non-nil, enables single-row selection.
onDelete ((IndexSet) -> Void)? nil When provided, enables swipe-to-delete.
onMove ((IndexSet, Int) -> Void)? nil When provided, enables drag-to-reorder.
row @ViewBuilder (Element) -> RowContent required A view builder returning a view for each element.

Theming

  • List background: theme.colors.background
  • Row background: theme.colors.surface
  • Selection tint: theme.colors.primary
  • Separator color: theme.colors.border

Examples

Read-only list

DFList(contacts) { contact in
    DFListRow(contact.displayName, subtitle: contact.email, icon: "person.fill")
}

With selection

@State private var selectedContact: Contact.ID? = nil

DFList(contacts, selection: $selectedContact) { contact in
    DFListRow(contact.displayName, subtitle: contact.email)
}

With delete support

@State private var items = Item.samples

DFList(items, onDelete: { offsets in
    items.remove(atOffsets: offsets)
}) { item in
    DFListRow(item.title, subtitle: item.subtitle)
}

With reorder support

DFList(tasks, onDelete: { tasks.remove(atOffsets: $0) },
              onMove:   { tasks.move(fromOffsets: $0, toOffset: $1) }) { task in
    DFListRow(task.title, accessory: .checkmark(isOn: task.isComplete))
}

Sectioned list via multiple DFLists

ScrollView {
    ForEach(sections) { section in
        VStack(alignment: .leading, spacing: 0) {
            DFText(section.title, style: .caption)
                .foregroundColor(theme.colors.textSecondary)
                .padding(.horizontal, theme.spacing.lg)
                .padding(.vertical, theme.spacing.sm)

            DFList(section.items) { item in
                DFListRow(item.name)
            }
        }
    }
}

DFListRow

DFListRow is the standard list row. It composes icon, title, subtitle, and an accessory control into a consistent, accessible layout.

Initializer Variants

// 1. Title only
public init(_ title: String)

// 2. Title + subtitle
public init(_ title: String, subtitle: String)

// 3. Title + subtitle + icon (SF Symbol)
public init(_ title: String, subtitle: String? = nil, icon: String)

// 4. Title + subtitle + leading view (arbitrary)
public init<Leading: View>(
    _ title: String,
    subtitle: String? = nil,
    @ViewBuilder leading: () -> Leading
)

Accessories

Apply with the accessory: label argument or .dfListRowAccessory(_:) modifier.

public enum DFListRowAccessory: Sendable {
    case navigation                // Trailing chevron
    case checkmark(isOn: Bool)     // Checkmark or blank
    case toggle(Binding<Bool>)     // Inline switch
    case badge(DFBadgeContent)     // Badge view
    case custom(AnyView)           // Arbitrary trailing view
}

Layout and Spacing

  • Row height: min(44, content) — at least 44 pt tall for HIG compliance
  • Horizontal padding: theme.spacing.lg
  • Icon size: 20 × 20 pt (DFIconSize.md)
  • Vertical padding: theme.spacing.md

Accessibility

DFListRow uses .accessibilityElement(children: .combine) — screen readers announce icon + title + subtitle as a single phrase.

When accessory is .toggle, the toggle is focusable independently and announced separately.

Examples

// Minimal
DFListRow("Files")

// Title + subtitle
DFListRow("Backup", subtitle: "Last run 3 minutes ago")

// With icon
DFListRow("Messages", subtitle: "No new messages", icon: "message.fill")

// Navigation accessory
DFListRow("Account Settings")
    .dfListRowAccessory(.navigation)

// Checkmark accessory
DFListRow("Use iCloud", accessory: .checkmark(isOn: useICloud))

// Toggle accessory
DFListRow("Dark mode", accessory: .toggle($isDarkMode))

// Badge accessory
DFListRow("Notifications", accessory: .badge(.count(5)))

// Custom leading content
DFListRow(
    "Alex Nguyen",
    subtitle: "Owner",
    leading: {
        DFAvatar(url: user.avatarURL, name: user.displayName, size: 36)
    }
)

DFTable

DFTable renders a read-only, sortable, themed table. Columns are defined with DFTableColumn descriptors. Use DFDataTable (see below) for interactive selection, and DFDataGrid for editable cells.

Data Types

public struct DFTableColumn<Row: Identifiable>: Sendable {
    public let id:         String
    public let header:     String
    public let width:      DFColumnWidth
    public let sortable:   Bool
    public var cell:       @Sendable (Row) -> AnyView
}

public enum DFColumnWidth: Sendable {
    case fixed(CGFloat)
    case flexible(min: CGFloat = 60, max: CGFloat = .infinity)
    case fill                // Takes remaining available width
}

Sort Behavior

DFTable maintains its own sort state internally. When the user taps a sortable column header, it calls the onSort closure:

DFTable(
    columns: columns,
    rows: rows,
    onSort: { columnId, ascending in
        sortRows(by: columnId, ascending: ascending)
    }
)

Examples

struct Transaction: Identifiable, Sendable {
    let id: UUID
    let date: String
    let description: String
    let amount: String
    let status: String
}

let columns: [DFTableColumn<Transaction>] = [
    DFTableColumn(id: "date",        header: "Date",        width: .fixed(100),    sortable: true)  { AnyView(Text($0.date)) },
    DFTableColumn(id: "description", header: "Description", width: .fill,          sortable: true)  { AnyView(Text($0.description)) },
    DFTableColumn(id: "amount",      header: "Amount",      width: .fixed(90),     sortable: false) { AnyView(Text($0.amount)) },
    DFTableColumn(id: "status",      header: "Status",      width: .flexible(min: 80, max: 120), sortable: false) {
        AnyView(DFBadge(text: $0.status, color: statusColor($0.status)))
    },
]

DFTable(columns: columns, rows: transactions) { columnId, ascending in
    transactions.sort(by: { compare($0, $1, by: columnId, ascending: ascending) })
}

DFDataTable

DFDataTable adds single-row or multi-row selection to the DFTable rendering. Platform-adaptive: on macOS it uses native table row selection styles; on iOS it highlights the selected row with theme.colors.primary.opacity(0.1).

Selection Modes

public enum DFDataTableSelectionMode: Sendable {
    case none
    case single(Binding<String?>)      // Selected row ID
    case multiple(Binding<Set<String>>) // Selected row IDs
}

Keyboard Navigation

On macOS, DFDataTable participates in focusable row navigation:

  • Arrow keys move selection
  • Space bar toggles selection in .multiple mode
  • Return activates the selection action if provided

Examples

// Single selection with action on double-tap
DFDataTable(
    columns: columns,
    rows: invoices,
    selection: .single($selectedInvoiceId)
) { invoiceId in
    openInvoice(id: invoiceId)
}

// Multi-select with bulk action
DFDataTable(
    columns: memberColumns,
    rows: members,
    selection: .multiple($selectedMemberIds)
)

DFDataGrid

DFDataGrid is an editable, paginated, sortable table. It extends DFDataTable with inline cell editing and large-dataset strategies for when your data exceeds what SwiftUI can render synchronously.

Column Descriptor

public struct DFDataGridColumn<Row: Identifiable>: Sendable {
    public let id:        String
    public let header:    String
    public let width:     DFColumnWidth
    public let sortable:  Bool
    public var editable:  Bool
    public var validator: ((String) -> DFValidationState)?
    public var cell:      @Sendable (Row) -> AnyView
    public var editor:    @Sendable (Row, @escaping (String) -> Void) -> AnyView
}

Large-Dataset Strategy

public enum DFDataGridLargeDatasetStrategy: Sendable {
    case pagination(pageSize: Int)     // Paginated pages with prev/next controls
    case virtualScrolling              // LazyVStack-backed, rows de-allocated off screen
    case none                          // All rows rendered; use for < 500 rows
}

Initializers

// In-memory data
public init<Row: Identifiable & Sendable>(
    columns:           [DFDataGridColumn<Row>],
    rows:              [Row],
    selection:         DFDataTableSelectionMode = .none,
    largeDataStrategy: DFDataGridLargeDatasetStrategy = .none,
    onEdit:            ((Row, String, String) -> Void)? = nil,
    onDelete:          ((IndexSet) -> Void)? = nil
)

// Async data source
public init<Row: Identifiable & Sendable>(
    columns:           [DFDataGridColumn<Row>],
    dataSource:        DFDataGridDataSource<Row>,
    selection:         DFDataTableSelectionMode = .none,
    largeDataStrategy: DFDataGridLargeDatasetStrategy = .pagination(pageSize: 50)
)

Full Parameter Table

Parameter Type Default Description
columns [DFDataGridColumn<Row>] required Column definitions
rows [Row] required (sync init) Data rows
dataSource DFDataGridDataSource<Row> required (async init) Async data provider
selection DFDataTableSelectionMode .none Selection mode
largeDataStrategy DFDataGridLargeDatasetStrategy .none Large data handling
onEdit ((Row, String, String) -> Void)? nil Callback: row, column ID, new value
onDelete ((IndexSet) -> Void)? nil Called with row indices to delete

Examples

Editable user table

let columns: [DFDataGridColumn<User>] = [
    DFDataGridColumn(
        id: "name", header: "Name", width: .flexible(), sortable: true,
        editable: true, validator: { v in v.isEmpty ? .error("Required") : .valid },
        cell: { AnyView(Text($0.name)) },
        editor: { user, commit in
            AnyView(TextField("Name", text: .constant(user.name), onCommit: { commit(user.name) }))
        }
    ),
    DFDataGridColumn(
        id: "email", header: "Email", width: .flexible(), sortable: true,
        editable: true, validator: { v in v.contains("@") ? .valid : .error("Invalid") },
        cell: { AnyView(Text($0.email)) },
        editor: { user, commit in
            AnyView(TextField("Email", text: .constant(user.email), onCommit: { commit(user.email) }))
        }
    ),
    DFDataGridColumn(
        id: "role", header: "Role", width: .fixed(120), sortable: false,
        editable: false, cell: { AnyView(DFBadge(text: $0.role)) }, editor: { _, _ in AnyView(EmptyView()) }
    ),
]

DFDataGrid(
    columns: columns,
    rows: users,
    selection: .multiple($selectedUserIds),
    onEdit: { user, columnId, newValue in updateUser(user, column: columnId, value: newValue) },
    onDelete: { users.remove(atOffsets: $0) }
)

Paginated async grid

DFDataGrid(
    columns: logColumns,
    dataSource: LogDataSource(api: logsAPI),
    largeDataStrategy: .pagination(pageSize: 100)
)

DFProgressBar

DFProgressBar shows linear or circular task completion state.

Initializer

public init(value: Double, total: Double = 1.0, label: String? = nil)

value must be in 0...total. Progress is clamped automatically.

Styles

Style Token Description
.linear .linear Full-width horizontal bar. Default.
.linearLabeled .linearLabeled Horizontal bar with label above and percentage right.
.circular .circular Ring progress indicator at 44 × 44 pt.
.glass .glass Linear bar with material backing. iOS 26+.

Apply with .dfProgressBarStyle(_:).

Accessibility

DFProgressBar applies .accessibilityValue("\(Int(value / total * 100))%") and .accessibilityLabel(label ?? "Progress") automatically.

Examples

// Simple linear
DFProgressBar(value: 0.68)

// Labeled
DFProgressBar(value: completedSteps, total: totalSteps, label: "Setup")
    .dfProgressBarStyle(.linearLabeled)

// Circular
DFProgressBar(value: bytesDownloaded, total: totalBytes)
    .dfProgressBarStyle(.circular)

// Themed color override
DFProgressBar(value: storageUsed, total: storageTotal)
    .foregroundColor(storageUsed > 0.9 * storageTotal ? theme.colors.error : theme.colors.primary)

// Animated progress
DFProgressBar(value: progress)
    .animation(.linear(duration: 0.3), value: progress)

DFSkeleton

DFSkeleton is a shimmer-animated placeholder for content that is still loading. Use it to preserve layout space and reduce perceived load time.

Initializer

public init(
    width:  CGFloat? = nil,  // nil = fills available width
    height: CGFloat,
    shape:  DFSkeletonShape = .roundedRect
)

public enum DFSkeletonShape: Sendable {
    case rectangle
    case roundedRect       // uses theme.radius.sm
    case circle
    case pill              // capsule shape
}

Shimmer Animation

The shimmer uses a left-to-right gradient sweep animating at theme.animation.slow. It is automatically disabled when accessibilityReduceMotion is enabled — the shimmer is replaced by a static low-opacity fill.

Styles

Style Token Description
.default default Gradient shimmer over theme.colors.border fill.
.dim .dim Darker, lower-contrast shimmer for light surfaces.
.glass .glass Material-backed shimmer. iOS 26+.

Examples

// Single text line
DFSkeleton(width: 180, height: 16)

// Circle avatar
DFSkeleton(width: 44, height: 44, shape: .circle)

// Full-width paragraph block
DFSkeleton(height: 12)
DFSkeleton(width: 260, height: 12)
DFSkeleton(width: 200, height: 12)

// Card skeleton composite
DFCard {
    VStack(alignment: .leading, spacing: 12) {
        HStack(spacing: 12) {
            DFSkeleton(width: 44, height: 44, shape: .circle)
            VStack(alignment: .leading, spacing: 6) {
                DFSkeleton(width: 120, height: 14)
                DFSkeleton(width: 80,  height: 12)
            }
        }
        DFSkeleton(height: 12)
        DFSkeleton(width: 220, height: 12)
        DFSkeleton(width: 180, height: 12)
    }
}

// List row skeletons while loading
if isLoading {
    ForEach(0..<5) { _ in
        HStack(spacing: 12) {
            DFSkeleton(width: 44, height: 44, shape: .circle)
            VStack(alignment: .leading, spacing: 6) {
                DFSkeleton(width: 160, height: 14)
                DFSkeleton(width: 100, height: 12)
            }
        }
        .padding(.horizontal, theme.spacing.lg)
    }
} else {
    DFList(contacts) { contact in
        DFListRow(contact.name, subtitle: contact.email, icon: "person.fill")
    }
}

Component Selection Guide

Requirement Component
Simple scrollable list with swipe-to-delete DFList
Standard list row with icon, title, subtitle DFListRow
Read-only sortable table DFTable
Sortable table with row selection DFDataTable
Editable table with validation, pagination DFDataGrid
Task or upload progress DFProgressBar
Loading state placeholder DFSkeleton

See also: Layout · Primitives · Theming · Forms-and-Validation · Getting-Started

Clone this wiki locally