SkillAgentSearch skills...

SwiftTabler

A multi-platform SwiftUI component for tabular data

Install / Use

/learn @openalloc/SwiftTabler

README

SwiftTabler

A multi-platform SwiftUI component for tabular data.

Available as an open source library to be incorporated in SwiftUI apps.

SwiftTabular is part of the OpenAlloc family of open source Swift software tools.

macOS | iOS :---:|:---: |

Features

  • Convenient display of tabular data from RandomAccessCollection data sources
  • Presently targeting macOS v11+ and iOS v14+*
  • Supporting both value and reference semantics (including Core Data, which uses the latter)
  • Option to support a bound data source, where inline controls can directly mutate your data model
  • Support for single-select, multi-select, or no selection
  • Option to specify a header and/or footer
  • Option to sort by column in header/footer, with indicators and concise syntax
  • Option to specify a row background and/or overlay
  • On macOS, option for hover events, such as to highlight row under the mouse cursor
  • MINIMAL use of View erasure (i.e., use of AnyView), which can impact scalability and performance**
  • No external dependencies!

Three table types are supported, as determined by the mechanism by which their header and rows are rendered.

List

  • Based on SwiftUI's List
  • Option to support moving of rows through drag and drop
  • Header/Footer are inside scrolling region

Stack

  • Based on ScrollView/LazyVStack
  • Header/Footer are outside scrolling region

Grid

  • Based on ScrollView/LazyVGrid
  • Likely the most scalable and efficient, but least flexible
  • Header/Footer are outside scrolling region

* Other platforms like macCatalyst, iPad on Mac, watchOS, tvOS, etc. are poorly supported, if at all. Please contribute to improve support!

** AnyView only used to specify sort images in configuration, which shouldn't impact scalability.

Tabler Example

The example below shows the display of tabular data from an array of values using TablerList, a simple variant based on List.

import SwiftUI
import Tabler

struct Fruit: Identifiable {
    var id: String
    var name: String
    var weight: Double
    var color: Color
}

struct ContentView: View {

    @State private var fruits: [Fruit] = [
        Fruit(id: "🍌", name: "Banana", weight: 118, color: .brown),
        Fruit(id: "🍓", name: "Strawberry", weight: 12, color: .red),
        Fruit(id: "🍊", name: "Orange", weight: 190, color: .orange),
        Fruit(id: "🥝", name: "Kiwi", weight: 75, color: .green),
        Fruit(id: "🍇", name: "Grape", weight: 7, color: .purple),
        Fruit(id: "🫐", name: "Blueberry", weight: 2, color: .blue),
    ]
    
    private var gridItems: [GridItem] = [
        GridItem(.flexible(minimum: 35, maximum: 40), alignment: .leading),
        GridItem(.flexible(minimum: 100), alignment: .leading),
        GridItem(.flexible(minimum: 40, maximum: 80), alignment: .trailing),
        GridItem(.flexible(minimum: 35, maximum: 50), alignment: .leading),
    ]

    private typealias Context = TablerContext<Fruit>

    private func header(ctx: Binding<Context>) -> some View {
        LazyVGrid(columns: gridItems) {
            Text("ID")
            Text("Name")
            Text("Weight")
            Text("Color")
        }
    }
    
    private func row(fruit: Fruit) -> some View {
        LazyVGrid(columns: gridItems) {
            Text(fruit.id)
            Text(fruit.name).foregroundColor(fruit.color)
            Text(String(format: "%.0f g", fruit.weight))
            Image(systemName: "rectangle.fill").foregroundColor(fruit.color)
        }
    }

    var body: some View {
        TablerList(header: header,
                   row: row,
                   results: fruits)
    }
}

While LazyVGrid is used here to wrap the header and row items, you could alternatively wrap them with HStack or similar mechanism.

Tabler Views

Tabler offers twenty-seven (27) variants of table views from which you can choose. They break down along the following lines:

  • Table View - the View name
  • Type - each of the three table types differ in how they render:
    • List - based on List
    • Stack - based on ScrollView/LazyVStack
    • Grid - based on ScrollView/LazyVGrid
  • Select - single-select, multi-select, or no selection
  • Value - if checked, can be used with value types (e.g., struct values)
  • Reference - if checked, can be used with reference types (e.g., class objects, Core Data, etc.)
  • Bound - if checked, can be used with inline controls (TextField, etc.) to mutate model
  • Filter - if checked, config.filter is supported (see caveat below)

Table View | Type | Select | Value | Reference | Bound | Filter :--- | :--- | :--- | :---: | :---: | :---: | :---:
TablerList | List | | ✓ | ✓ | | ✓
TablerListB | List | | ✓ | | ✓ | ✓*
TablerListC | List | | | ✓ | ✓ |
TablerList1 | List | Single | ✓ | ✓ | | ✓
TablerList1B | List | Single | ✓ | | ✓ | ✓*
TablerList1C | List | Single | | ✓ | ✓ |
TablerListM | List | Multi | ✓ | ✓ | | ✓
TablerListMB | List | Multi | ✓ | | ✓ | ✓*
TablerListMC | List | Multi | | ✓ | ✓ |
TablerStack | Stack | | ✓ | ✓ | | ✓
TablerStackB | Stack | | ✓ | | ✓ | ✓*
TablerStackC | Stack | | | ✓ | ✓ |
TablerStack1 | Stack | Single | ✓ | ✓ | | ✓
TablerStack1B | Stack | Single | ✓ | | ✓ | ✓*
TablerStack1C | Stack | Single | | ✓ | ✓ |
TablerStackM | Stack | Multi | ✓ | ✓ | | ✓
TablerStackMB | Stack | Multi | ✓ | | ✓ | ✓*
TablerStackMC | Stack | Multi | | ✓ | ✓ |
TablerGrid | Grid | | ✓ | ✓ | | ✓
TablerGridB | Grid | | ✓ | | ✓ |
TablerGridC | Grid | | | ✓ | ✓ |
TablerGrid1 | Grid | Single | ✓ | ✓ | | ✓
TablerGrid1B | Grid | Single | ✓ | | ✓ |
TablerGrid1C | Grid | Single | | ✓ | ✓ |
TablerGridM | Grid | Multi | ✓ | ✓ | | ✓
TablerGridMB | Grid | Multi | ✓ | | ✓ |
TablerGridMC | Grid | Multi | | ✓ | ✓ |

* filtering with bound values likely not scalable as implemented. If you can find a better way to implement, please submit a pull request!

Header/Footer

Optionally attach a header (or footer) to your table:

var body: some View {
    TablerList(header: header,
               footer: footer,
               row: row,
               results: fruits)
}

private func header(ctx: Binding<Context>) -> some View {
    LazyVGrid(columns: gridItems) {
        Text("ID")
        Text("Name")
        Text("Weight")
        Text("Color")
    }
}

private func footer(ctx: Binding<Context>) -> some View {
    LazyVGrid(columns: gridItems) {
        Text("ID")
        Text("Name")
        Text("Weight")
        Text("Color")
    }
}

Where you don't want a header (or footer), simply omit from the declaration of the table.

For List based variants, the header and footer are inside the scrolling region. For Stack and Grid based variants, they are outside. (This may be configurable at some point once any scaling/performance issues are resolved.)

Column Sorting

Column sorting is available through the tablerSort view function.

The examples below show how the header items can support sort.

.columnTitle() is a convenience function that displays header name along with an indicator showing the current sort state, if any. Alternatively, build your own header and call the .indicator() method to get the active indicator image.

Caret images are used by default for indicators, but are configurable (see Configuration section below).

Random Access Collection

From the TablerDemo app:

private typealias Context = TablerContext<Fruit>
private typealias Sort = TablerSort<Fruit>

private func header(ctx: Binding<Context>) -> some View {
    LazyVGrid(columns: gridItems) {
        Sort.columnTitle("ID", ctx, \.id)
            .onTapGesture { tablerSort(ctx, &fruits, \.id) { $0.id < $1.id } }
        Sort.columnTitle("Name", ctx, \.name)
            .onTapGesture { tablerSort(ctx, &fruits, \.name) { $0.name < $1.name } }
        Sort.columnTitle("Weight", ctx, \.weight)
            .onTapGesture { tablerSort(ctx, &fruits, \.weight) { $0.weight < $1.weight } }
        Text("Color")
    }
}

Core Data

The sort method used with Core Data differs. From the TablerCoreDemo app:

private typealias Context = TablerContext<Fruit>
private typealias Sort = TablerSort<Fruit>

private func header(ctx: Binding<Context>) -> some View {
    LazyVGrid(columns: gridItems, alignment: .leading) {
        Sort.columnTitle("ID", ctx, \.id)
            .onTapGesture { fruits.sortDescriptors = [tablerSort(ctx, \.id)] }
        Sort.columnTitle("Name", ctx, \.name)
            .onTapGesture { fruits.sortDescriptors = [tablerSort(ctx, \.name)] }
        Sort.columnTitle("Weight", ctx, \.weight)
            .onTapGesture { fruits.sortDescriptors = [tablerSort(ctx, \.weight)] }
    }
}

Sorting on a computed column

Where there is no key path a

View on GitHub
GitHub Stars144
CategoryDevelopment
Updated16d ago
Forks17

Languages

Swift

Security Score

100/100

Audited on Mar 17, 2026

No findings