Skip to content
ngtablePublic

About

A fast table for macOS and iPad: GPU-drawn cells, frozen columns, summaries with formulas, grouping, filters, live updates. One NSTableView-like API for AppKit, UIKit and SwiftUI.

Topics

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

NGTable

macOS 14+ iPadOS 17+ Swift 6.2+ SwiftPM License

A fast table for business data on macOS and iPad: frozen columns, column groups, a summary row with formulas, row grouping with subtotals, search and filters, live updates, and a selection that stays on the same records across reloads. The GPU draws it, and its API mirrors NSTableView, so apps migrate without reworking their patterns. One NGTableView class works in AppKit and in UIKit, with the same data source, delegate and features; on the iPad it takes touches, the pointer, a hardware keyboard and VoiceOver as the iPad's own controls do.

Note

Free for noncommercial use (personal projects, study, research, schools, universities, charities). Using it in a business needs a commercial license, including internal tools: write to d.polikhranidi+ngtable@gmail.com. See Licensing.

Why NGTable

NGTable, NSTableView and SwiftUI Table showing the same live trades

20,000 live trades sorted by amount, 45,000 changes a second, in each table in turn (a Release build on Apple silicon, 120 Hz screen). NGTable sorts the rows itself and moves only the changed ones; NSTableView and SwiftUI's Table leave sorting to the app, which sorts again after each change. The app is the NGTable Comparison scheme of NGTable.xcworkspace (see Demos and benches).

More scenarios, measured the same way. Main thread: the share of time it was busy (less is more room for your app). Frames: screen updates a second.

Scenario NGTable NSTableView SwiftUI Table
Live trades sorted by amount: 20,000 rows, 45,000 changes a second ~15% · 120 frames 97% · 17 frames 99% · 3 frames
Order book: 200,000 rows, 244,000 changes a second 51% · 120 frames 97% · 28 frames doesn't load the rows
Trade feed: 1,000,000 rows, 300,000 changes a second 61% · 120 frames 97% · 15 frames doesn't load the rows
Binance live tickers: 3,100 pairs 58% · 120 frames 100% · 4 frames 99% · 18 frames
Binance order books: 58,000 levels 17% · 118 frames 99% · 7 frames 100% · 0 frames
Sorting 1,000,000 rows by a text column 0.34 s with NGTableICU (2 s without), the screen keeps drawing 1.9 s, the app stops for it —

NGTable also sorts, filters, sums and highlights changes while doing it.

On the iPad, the bench in Demos/iPadBench shows the same live rows in NGTable, UITableView, UICollectionView and SwiftUI's Table and List, and scrolls each down and up as fast as a flick for eight seconds. In the simulator (a Release build, 100,000 rows, 1,000 changes a second):

Scrolling, values changing NGTable SwiftUI Table SwiftUI List UICollectionView UITableView
Frames a second (the worst second) 60 (60) 53 (37) 52 (40) 38 (14) 25 (3)
Longest frame 18 ms 83 ms 83 ms 217 ms 496 ms

With a million rows and 10,000 changes a second NGTable still gets 60 frames (59 in the worst second). The simulator has the Mac's keyboard attached, and with a keyboard iPadOS's focus system slows UIKit's tables while they scroll; measure on your device too (the bench's launch arguments run the test by themselves).

Features

  • Frozen columns at the leading and trailing edges, and column groups under a common header.
  • Summary row: sums, averages, counts, minimums and maximums, value filters and formulas over other columns (e.g. ROI as a ratio of sums), for all rows, the selected rows or all except the selected ones. Large tables are summarized in the background.
  • Row grouping by column values, several levels deep, under collapsible header rows with row counts and subtotals.
  • Search and filters that hide rows without touching the data source.
  • Row identity: the selection follows the same records when data is sorted or refreshed.
  • Live data: targeted updates (reloadData(forRowIndexes:columnIndexes:), inserts, removes, moves, batched with beginUpdates()/endUpdates()) cost what changed, also with 200,000 rows; reorderRows(from:to:) re-sorts without reading the rows again.
  • NSTableView's patterns: the same data source, delegate, sort descriptor and notification names, a diffable data source like NSTableViewDiffableDataSource, and an adapter for an existing NSTableViewDataSource.
  • Context menus built by the app, with Finder's rules for which rows a click is for.
  • Column customization: resize, move, hide and show columns, saved and restored.
  • Values as text, numbers, currencies, percentages, dates, check marks, colored tags, icon marks, sparklines (lines, areas, bars, wins and losses), links, buttons, progress bars, ratings, and pictures and videos by URL (loaded, cached and played under the pointer by the table); text that wraps, rows as high as it.
  • Native feel: overlay scrollers, elastic scrolling, Liquid Glass summary bar on macOS 26, dark mode, accent colors, keyboard navigation, type select, copying to spreadsheets, VoiceOver.
  • On the iPad: momentum scrolling, taps that select and act like clicks, editing in place, context menus on a long press, swipe actions, columns resized and moved by a finger, filters and choices in popovers, drag and drop, export to the share sheet, the pointer's hover, a hardware keyboard, VoiceOver's data table, and the summary row above the home indicator.
  • Fast with a million rows and hundreds of thousands of changes a second: the GPU (Metal) draws the rows from a glyph atlas, with Core Text's typesetting; changing a value costs no rasterization, and frames are drawn only when something visible changed.

Your app owns the data. The table sorts the rows itself (sortsRows, in the background for large tables) or leaves sorting to your app like NSTableView, and your app performs the actions of its buttons and menus.

Important

Sorting large text columns? Add NGTableICU. NGTable works on its own and sorts text like Finder on all cores: a million shuffled texts in about 3 seconds, in the background. With the optional NGTableICU library the same sort takes about 0.6 seconds, in the same order, but your app grows by about 9 MB per architecture (ICU's code and its collation data: about 17 MB in a universal Intel + Apple silicon app). It is safe for the Mac App Store. Add the library (a second product of this package) and one line at launch:

import NGTableICU
NGTableView.defaultCollationKeyProvider = NGBundledICUCollation()

Tables of a few thousand rows don't need it: they sort in milliseconds either way. See "Sorting a million rows" in the documentation.

In SwiftUI on the Mac, NGDataTable works like Table (on the iPad it comes next; until then wrap NGTableView in a UIViewRepresentable):

NGDataTable(campaigns, selection: $selection) {
    NGDataColumn("Name", value: \.name)
    NGDataColumn("Spend", value: \.spend, format: .currency(code: "USD"), summary: .aggregate(.sum))
}

Requirements

  • macOS 14 or later (Liquid Glass on macOS 26 and later)
  • iPadOS 17 or later (the table view in UIKit; it builds for the iPhone too, but is made for the iPad's width)
  • Swift 6.2 or later (Xcode 26 or later)

Installation

In Xcode, choose File > Add Package Dependencies, enter the repository's URL, and add the NGTable library to your app target. In a Package.swift:

dependencies: [
    .package(url: "https://github.com/ngtable/NGTable", from: "1.3.0"),
],
targets: [
    .target(name: "MyApp", dependencies: ["NGTable"]),
]

For fast sorts of large text columns, also add the optional NGTableICU library, .product(name: "NGTableICU", package: "NGTable") (about 9 MB per architecture, see the note above). Apps without it don't build its ICU.

Versions

NGTable follows semantic versioning, with one rule for enums: a minor version may add cases to the public enums that describe values, formats, filters and summaries (NGValueFormat, NGColumnFilter, NGCellValue, NGAggregate and the like), as 1.1 added the .dates, .top and .bottom filters. Keep a default in a switch over them, and updates won't stop your code from compiling. Only NGColumnItem, NGDataColumnItem, NGColumnPin and NGColumnVisibility.State keep their cases until the next major version.

Tip

In the binary NGTable.xcframework (XCFramework/build-xcframework.sh, built with library evolution) enums may get new cases: a switch over one needs @unknown default. Only NGColumnItem, NGDataColumnItem, NGColumnPin and NGColumnVisibility.State are @frozen and switch without it.

Quick start

import AppKit
import NGTable

struct Campaign {
    var id: String
    var name: String
    var spend: Double
}

@MainActor
final class CampaignTableData: NGTableViewDataSource {
    var campaigns: [Campaign] = []

    func numberOfRows(in tableView: NGTableView) -> Int {
        campaigns.count
    }

    func tableView(_ tableView: NGTableView, valueFor column: NGTableColumn, row: Int) -> NGCellValue {
        let campaign = campaigns[row]
        switch column.id {
        case "id": return .text(campaign.id)
        case "name": return .text(campaign.name)
        case "spend": return .number(campaign.spend)
        default: return .empty
        }
    }
}

final class CampaignsViewController: NSViewController {
    private let data = CampaignTableData()
    private let table = NGTableView(frame: .zero)

    override func loadView() {
        table.columns = [
            .column(NGTableColumn("name", title: "Campaign", width: 240, pin: .leading)),
            .column(NGTableColumn("spend", title: "Spend", width: 120, alignment: .automatic,
                                  format: .currency(code: "EUR"), summary: .aggregate(.sum))),
        ]
        table.rowIdentifierColumnID = "id"   // The selection follows the same campaigns.
        table.autosaveName = "Campaigns"     // The user's columns are remembered.
        table.dataSource = data
        view = table
    }

    func show(_ campaigns: [Campaign]) {
        data.campaigns = campaigns
        table.reloadData()                   // Always right after changing the data.
    }
}

On the iPad the same data source makes the table; put it in a UIViewController (view = table, or as a subview pinned to the edges: it keeps its summary row above the home indicator by itself). What the Mac does with the mouse the iPad does by touch:

On the Mac On the iPad
Click, Command- and Shift-click Tap (Command and Shift with a hardware keyboard)
Double click Double tap (edits an editable cell)
Right-click menu: tableView(_:menuForRows:clickedRow:column:) returns an NSMenu Long press: the same method returns a UIMenu
Trackpad swipe across a row Finger swipe (where the table can't scroll further sideways)
Dragging a column's edge or title The same, by a finger
Filter button, Option-Down Arrow The header menu's Filter item, the filter button of a filtered column
pasteboardWriterForRow, validateDrop, acceptDrop itemsForBeginning, dropSessionDidUpdate, performDrop (like UITableView's)
exportDocument(_:): a save panel exportDocument(_:): the share sheet

Rules that matter

  1. Call reloadData() immediately after changing the data, before selecting, scrolling or returning to the run loop. Until then the table still asks about the old rows.
  2. Row indexes in the API (selection, clickedRow, data source and delegate calls) are data source indexes, also while a filter hides rows. They are only valid until the next reload: keep record ids, not indexes, in menus and asynchronous work.
  3. Give rows an identity (rowIdentifierColumnID or tableView(_:identifierForRow:)), unique per record.
  4. The table holds its data source and delegate weakly; capture self weakly in NGRowFilter.isIncluded.
  5. Keep tableView(_:valueFor:row:) a cheap lookup, and return static color constants for row tints and markers.
  6. Don't put the table in an NSScrollView or a UIScrollView: it scrolls by itself.

Documentation

The package has full DocC documentation: API reference for every public type and member, and articles (written for the Mac; on the iPad the API is the same, and the table above maps the interactions):

Article Covers
Getting Started Adding the package, columns, data source, showing and reloading
Integrating into an App A complete CRM screen: model, data source, sorting, search, filters, menus, refresh, live updates, checklist
Migrating from NSTableView The NSTableView → NGTableView mapping, the data source adapter, diffable data sources, what works differently
Defining Columns Options, frozen columns, groups, icon columns, user customization, saving layouts
Displaying Values Value kinds, formats, tags, marks, sparklines, links, buttons, progress bars, ratings, pictures and videos, the pointer over cells
Summaries Aggregates, value filters, formulas, scopes, group subtotals, large tables
Filtering and Search Row filters, search matching, row indexes while filtering, empty states
Grouping Rows Groups by column values, header rows with subtotals, collapsing, order of groups
Selection and Sorting Selection API, row identity, sorting, re-sorting without reloading
Menus and Actions Row context menus, actions, copying
Appearance Sizes, row tints and markers, selection style, scrolling
Keyboard and Accessibility Keyboard commands, type select, VoiceOver
Performance and Threading GPU rendering, what the table reads and when, large data, live updates, memory

To read it in Xcode, open the package (or your app that uses it) and choose Product > Build Documentation; it opens in the documentation window. Symbol documentation also shows in Quick Help (Option-click a symbol). The sources are in Sources/NGTable/NGTable.docc.

Demos and benches

Open NGTable.xcworkspace: the package (the libraries, their tests and NGTableDemo) and every app below, in one window. Choose a scheme and run it (Command-R); the benches' schemes run Release builds, as measuring needs.

Scheme What it is
NGTableDemo A small Mac app with ad campaign data: frozen and grouped columns, marks, a ROI formula, search, a filter, summary scopes, row tints, a context menu and 0 to 100,000 rows. Its source, Sources/NGTableDemo/NGTableDemo.swift, is a working example of most of the API.
NGTable Comparison The same live trades in NGTable, NSTableView and SwiftUI's Table in turn (the animation above).
NGTable Mac Bench The Mac's benches, a tab each: live data that changes by itself (updates, sorting, grouping), a real app's campaigns table with synthetic campaigns, a live feed over the network from an ngfeed server, and Binance's public market data; the feed and Binance can show NSTableView and SwiftUI's Table instead. Launch arguments open a tab and start it: -BenchAutostart YES, -FeedAutostart trading, -BinanceAutostart tickers.
NGTable Log Viewer The Mac's own logs as they come, or the last hour of them (about a million entries): wrapped messages, search, column filters, grouping, export, and the frames a second it gets meanwhile.
NGTable iPad The table on the iPad with live campaigns: editing, menus, swipe actions, filters, drag and drop, export to the share sheet, the frames a second.
NGTable iPad Bench NGTable, UITableView, UICollectionView and SwiftUI's Table and List on the same live rows (up to a million, up to 10,000 changes a second), with a scroll test; measure on a device too. Launch arguments run it by themselves: -engine 0…4 -rows 0…2 -rate 0…3 -autoTest YES.

The apps are in Demos/NGTableDemos.xcodeproj (made with XcodeGen from Demos/project.yml). The comparison and the log viewer also run from the command line:

swift run -c release --package-path Comparison NGTableComparison
swift run -c release --package-path Demos/LogViewer NGTableLogViewer --last 60

Tests

Run the tests with the NGTable-Package scheme in Xcode (Command-U), or swift test in the package folder. The same scheme runs them on an iPad simulator (the view tests that need AppKit are the Mac's; NGTouchTests drives the iPad's taps, menus, keyboard, VoiceOver, swipes and drag and drop).

Package layout

Path Contents
Sources/NGTable/NGTable.swift Data source and delegate protocols
Sources/NGTable/Model Columns, values, formats, summaries, filters, layouts (no AppKit or UIKit)
Sources/NGTable/Table NGTableView for both platforms (the class in NGTableView.swift, its parts in NGTable*.swift extensions): layout, scrolling, selection, sorting, row grouping, updates, filters, export, editing, and what the interactions do (menus' items, keys, column moves, swipes, drops)
Sources/NGTable/AppKit The Mac's views and its mouse, keyboard and menu events, scrollers, accessibility elements, the filter popover, printing
Sources/NGTable/UIKit The iPad's views and its touches, pointer, keys, context menus, popovers, VoiceOver and drag and drop
Sources/NGTable/Metal The GPU renderer: shaders, the glyph atlas, Core Text shaping
Sources/NGTable/NGTable.docc Documentation articles
NGTable.xcworkspace The package and the demos project, in one window
Sources/NGTableDemo The demo app
Sources/NGTableICU The optional NGTableICU library (NGTableICU.md)
Vendor/CollationICU The ICU it is built with, made by Scripts/vendor-icu.py
Tests/NGTableTests Unit and view tests
Tests/NGTableICUTests NGTableICU's tests
XCFramework The binary frameworks' builds (ICU: NGTableICU's)
Comparison The engine comparison (Docs/comparison.gif), a package of its own too
Demos The apps' project (NGTableDemos.xcodeproj, XcodeGen's project.yml) and their sources: iPadTable, iPadBench, MacBench, LogViewer

License

NGTable is source-available, under two licenses: free for noncommercial use (personal projects, study, research, schools, universities, charities and other noncommercial organizations) under the PolyForm Noncommercial License 1.0.0, and a commercial license for use in a business, internal tools included. A commercial license covers a major version (all of 1.x), and apps shipped with it stay licensed. To buy one, write to d.polikhranidi+ngtable@gmail.com. See LICENSING.md; NGTableView.version tells which version you have.

Contributing

Bug reports, questions and ideas are welcome as issues; pull requests aren't taken for now. See CONTRIBUTING.md.

About

A fast table for macOS and iPad: GPU-drawn cells, frozen columns, summaries with formulas, grouping, filters, live updates. One NSTableView-like API for AppKit, UIKit and SwiftUI.

Topics

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages