GitHub

Fixed Columns

Freeze columns on the left or right while scrolling horizontally.

When your table has many columns, users scroll horizontally to see them all. But what happens to the identifier column — the one that tells you which row you're looking at? It scrolls off-screen, leaving users lost in a sea of data.

Fixed columns solve this by freezing columns in place. They stay anchored while the rest of the table scrolls beneath them, keeping context visible at all times.

What Users See

When you freeze a column, here's what happens as the user scrolls horizontally:

  • The fixed column stays perfectly still — it doesn't move a pixel
  • The other columns slide underneath it, disappearing beneath the fixed column's edge
  • A subtle shadow appears on the fixed column, creating a sense of depth — signaling that content is hidden beneath
  • As users scroll back, columns reappear from under the fixed column

The effect is like a pane of glass sitting above the table. The fixed column lives on top; everything else slides beneath it. This keeps your identifier column (like "Name" or "ID") visible no matter how far users scroll into the data.

How It Works Internally

  • Left-fixed columns anchor to the leading edge — they never move
  • Right-fixed columns anchor to the trailing edge — always visible on the right
  • Scrollable columns move freely between the fixed regions
  • Shadow appears dynamically when scrollable content passes beneath

Configuration

Set the fixedColumns property on DataTableConfiguration using one of three initializers:

Fix Left Columns

Freeze the first N columns. Perfect for ID, name, or any identifier column.

var config = DataTableConfiguration()
config.fixedColumns = DataTableFixedColumnType(leftColumns: 1)

Fix Right Columns

Freeze the last N columns. Useful for action buttons, status indicators, or totals.

config.fixedColumns = DataTableFixedColumnType(rightColumns: 2)

Fix Both Sides

Freeze columns on both edges. The middle columns scroll between them.

config.fixedColumns = DataTableFixedColumnType(leftColumns: 1, rightColumns: 1)

Real-World Examples

Employee Directory

Keep the employee name visible while scrolling through contact details, department info, and other fields.

struct Employee: Identifiable {
    let id: String
    let name: String
    let department: String
    let email: String
    let phone: String
    let office: String
    let startDate: Date
}

var config = DataTableConfiguration()
config.fixedColumns = DataTableFixedColumnType(leftColumns: 1)

let columns: [DataTableColumn<Employee>] = [
    .init("Name", \.name),         // Fixed — always visible
    .init("Department", \.department),
    .init("Email", \.email),
    .init("Phone", \.phone),
    .init("Office", \.office),
    .init("Started") { $0.startDate.formatted(date: .abbreviated, time: .omitted) }
]

Financial Spreadsheet

Fix the account name on the left and the total on the right. Monthly values scroll between them.

struct Account: Identifiable {
    let id: String
    let name: String
    let jan, feb, mar, apr, may, jun: Double
    var total: Double { jan + feb + mar + apr + may + jun }
}

var config = DataTableConfiguration()
config.fixedColumns = DataTableFixedColumnType(leftColumns: 1, rightColumns: 1)

let columns: [DataTableColumn<Account>] = [
    .init("Account", \.name),     // Fixed left
    .init("Jan", \.jan) { String(format: "$%.2f", $0) },
    .init("Feb", \.feb) { String(format: "$%.2f", $0) },
    .init("Mar", \.mar) { String(format: "$%.2f", $0) },
    .init("Apr", \.apr) { String(format: "$%.2f", $0) },
    .init("May", \.may) { String(format: "$%.2f", $0) },
    .init("Jun", \.jun) { String(format: "$%.2f", $0) },
    .init("Total", \.total) { String(format: "$%.2f", $0) }  // Fixed right
]

Task List with Actions

Keep the task name visible and pin action buttons on the right edge for quick access.

// Task name fixed left, action column fixed right
config.fixedColumns = DataTableFixedColumnType(leftColumns: 1, rightColumns: 1)

let columns: [DataTableColumn<Task>] = [
    .init("Task", \.title),        // Fixed left
    .init("Assignee", \.assignee),
    .init("Due") { $0.dueDate?.formatted() ?? "—" },
    .init("Priority", \.priority),
    DataTableColumn<Task>("Actions")  // Fixed right — use custom cell for buttons
]

Dynamic Control via Delegate

For dynamic scenarios where fixed columns depend on runtime conditions, implement the delegate method instead of using configuration:

func fixedColumns(for dataTable: SwiftDataTable) -> DataTableFixedColumnType? {
    // Only fix columns in landscape
    if UIDevice.current.orientation.isLandscape {
        return DataTableFixedColumnType(leftColumns: 2)
    }
    return nil  // No fixed columns in portrait
}

Freezing Too Many Columns

Fixed columns take up screen space. If you freeze too many, the scrollable area between them shrinks — potentially to nothing. Imagine a table with 6 columns on an iPhone in portrait mode. If you fix 2 on the left and 2 on the right, the remaining 2 scrollable columns might have only 50 points of horizontal space to scroll in, making the table feel broken or unusable.

Rule of thumb On iPhone, fix at most 1-2 columns total. On iPad, you have more room — 2-3 is usually safe. Always test on real devices.

Fixed Columns Still Sort

Freezing a column doesn't disable sorting. Tapping a fixed column header still sorts the table by that column. If you want to prevent sorting on fixed columns, use isColumnSortable:

config.fixedColumns = DataTableFixedColumnType(leftColumns: 1)
config.isColumnSortable = { columnIndex in
    columnIndex != 0  // First column (fixed) isn't sortable
}

Adapting to Device Size

What works on iPad may not work on iPhone. Use the delegate method to adjust based on available space:

func fixedColumns(for dataTable: SwiftDataTable) -> DataTableFixedColumnType? {
    let width = dataTable.bounds.width
    
    if width > 700 {
        // iPad landscape — room for both sides
        return DataTableFixedColumnType(leftColumns: 1, rightColumns: 1)
    } else if width > 400 {
        // iPad portrait or large iPhone — just left
        return DataTableFixedColumnType(leftColumns: 1)
    } else {
        // Small iPhone — no fixed columns
        return nil
    }
}