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
}
}