A multi-platform SwiftUI component for tabular data

Overview

SwiftTabler

A multi-platform SwiftUI component for tabular data.

NOTE this component is BRAND NEW and under active development. If you need stability, you should fork, at least until the API has stabilized with version 1.x.x.

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 a RandomAccessCollection or Core Data source
  • Presently targeting macOS v11+ and iOS v14+*
  • Supporting bound and unbound arrays, and Core Data too
  • With bound data, add inline controls to interactively change (and mutate) your data model
  • Optional sort-by-column support, with concise syntax
  • Optional support for colored rows, with selection overlay
  • MINIMAL use of View erasure (i.e., use of AnyView), which can impact scalability and performance**
  • No external dependencies!

For List-based tables:

  • Optional moving of rows through drag and drop
  • Support for no-select, single-select, and multi-select

For ScrollView/LazyVStack-based tables:

  • Support for no-select and single-select (possibily multi-select in future)

For ScrollView/LazyVGrid-based tables:

  • Likely the most scalable and efficient, but least flexible

On macOS:

  • Hovering highlight, indicating which row the mouse is over

* 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 configuration images in configuration, which shouldn't impact scalability.

Tabler Example

The basic example below shows the display of tabular data using TablerList, which is for the display of unbound data without any selection capability.

) -> some View { Text("ID") Text("Name") Text("Weight") Text("Color") } @ViewBuilder private func row(_ element: Fruit) -> some View { Text(element.id) Text(element.name).foregroundColor(element.color) Text(String(format: "%.0f g", element.weight)) Image(systemName: "rectangle.fill").foregroundColor(element.color) } var body: some View { TablerList(config, headerContent: header, rowContent: row, results: fruits) .padding() } private var config: TablerListConfig { TablerListConfig (gridItems: gridItems) } }">
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),
    ]

    @ViewBuilder
    private func header(_ ctx: TablerSortContext
     ) 
     -> 
     some View {
        
     Text(
     "ID")
        
     Text(
     "Name")
        
     Text(
     "Weight")
        
     Text(
     "Color")
    }
    
    
     @ViewBuilder
    
     private 
     func 
     row(
     _ 
     element: Fruit) 
     -> 
     some View {
        
     Text(element.
     id)
        
     Text(element.
     name).
     foregroundColor(element.
     color)
        
     Text(
     String(
     format: 
     "%.0f g", element.
     weight))
        
     Image(
     systemName: 
     "rectangle.fill").
     foregroundColor(element.
     color)
    }

    
     var body: 
     some View {
        
     TablerList(config,
                   
     headerContent: header,
                   
     rowContent: row,
                   
     results: fruits)
            .
     padding()
    }
    
    
     private 
     var config: TablerListConfig
     
       {
        TablerListConfig
      <Fruit
      >(
      gridItems: gridItems)
    }
}
     
    

Tables

You can choose from any of eleven (11) variants, which break down along the following lines:

  • List-based, ScrollView/LazyVStack-based, and ScrollView/LazyVGrid-based
  • Selection types offered: none, single-select, and multi-select, depending on base
  • Unbound elements in row view, where you're presenting table rows read-only*
  • Bound elements in row view, where you're presenting tables rows that can be updated directly (see Bound section below)
Base Selection of rows Element wrapping View name Notes
List No Select (none) TablerList
List No Select Binding TablerListB
List Single-select (none) TablerList1
List Single-select Binding TablerList1B
List Multi-select (none) TablerListM
List Multi-select Binding TablerListMB
Stack No Select (none) TablerStack
Stack No Select Binding TablerStackB
Stack Single-select (none) TablerStack1
Stack Single-select Binding TablerStack1B
Grid No Select (none) TablerGrid Experimental. Needs bound version, select, etc.

* 'unbound' variants can be used with Core Data (where values are bound by alternative means)

Column Sorting

Column sorting is available through tablerSort view function.

From the demo app, an example of using the sort capability, where an indicator displays in the header if the column is actively sorted:

private typealias Context = TablerContext

   private 
   typealias 
   Sort 
   = TablerSort
   


    @ViewBuilder

    private 
    func 
    header(
    _ 
    ctx: Binding
    
     ) 
     -> 
     some View {
    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")
}
    
   
  

When the user clicks on a header column for the first time, it is sorted in ascending order, with an up-chevron "^" indicator. If clicked a successive time, a descending sort is executed, with a down-chevron "v" indicator. See TablerConfig for configuration.

For sorting with Core Data, see the TablerCoreDemo app.

Bound data

macOS iOS

When used with 'bound' variants (e.g., TablerListB), the data can be modified directly, mutating your data source. From the demo:

@ViewBuilder
private func brow(_ element: Binding
   ) 
   -> 
   some View {
    
   Text(element.
   wrappedValue.
   id)
    
   TextField(
   "Name", 
   text: element.
   name)
        .
   textFieldStyle(.
   roundedBorder)
    
   Text(
   String(
   format: 
   "%.0f g", element.
   wrappedValue.
   weight))
    
   ColorPicker(
   "Color", 
   selection: element.
   color)
        .
   labelsHidden()
}
  

Colored Rows

macOS iOS

The demo app (link below) shows how colored rows are implemented.

Because the normal selection is obscured with colored rows, the ability to use a 'selection overlay' is provided. An example is available in the demo.

Disable Header

The demo app shows how to toggle the display of the header, where a header may not be desired.

Moving Rows

TODO add details here, with example of move action handler.

Horizontal Scrolling

On compact displays you may wish to scroll the table horizontally. TablerDemo does this through a ScrollView wrapper:

public struct SidewaysScroller<Content: View>: View {
    var minWidth: CGFloat
    @ViewBuilder var content: () -> Content

    public init(minWidth: CGFloat,
                @ViewBuilder content: @escaping () -> Content)
    {
        self.minWidth = minWidth
        self.content = content
    }

    public var body: some View {
        GeometryReader { geo in
            ScrollView(.horizontal) {
                VStack(alignment: .leading) {
                    content()
                }
                .frame(minWidth: max(minWidth, geo.size.width))
            }
        }
    }
}

See Also

  • TablerDemo - the demonstration app for this library, for RandomAccessCollection data sources
  • TablerCoreDemo - the demonstration app for this library, for Core Data sources

Swift open-source libraries (by the same author):

  • SwiftDetailer - multi-platform SwiftUI component for editing fielded data
  • AllocData - standardized data formats for investing-focused apps and tools
  • FINporter - library and command-line tool to transform various specialized finance-related formats to the standardized schema of AllocData
  • SwiftCompactor - formatters for the concise display of Numbers, Currency, and Time Intervals
  • SwiftModifiedDietz - A tool for calculating portfolio performance using the Modified Dietz method
  • SwiftNiceScale - generate 'nice' numbers for label ticks over a range, such as for y-axis on a chart
  • SwiftRegressor - a linear regression tool thatโ€™s flexible and easy to use
  • SwiftSeriesResampler - transform a series of coordinate values into a new series with uniform intervals
  • SwiftSimpleTree - a nested data structure thatโ€™s flexible and easy to use

And commercial apps using this library (by the same author):

  • FlowAllocator - portfolio rebalancing tool for macOS
  • FlowWorth - a new portfolio performance and valuation tracking tool for macOS

License

Copyright 2022 FlowAllocator LLC

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

Contributing

Contributions are welcome. You are encouraged to submit pull requests to fix bugs, improve documentation, or offer new features.

The pull request need not be a production-ready feature or fix. It can be a draft of proposed changes, or simply a test to show that expected behavior is buggy. Discussion on the pull request can proceed from there.

Comments
  • Support unselect for Stack- and Grid-based tables

    Support unselect for Stack- and Grid-based tables

    Presently there is no way to unselect a row, other than to select another row of the same table.

    Ideally this will work like the native single-select for List tables.

    bug 
    opened by reedes 2
  • Revisit paddingInsets and rowSpacing defaults in config

    Revisit paddingInsets and rowSpacing defaults in config

    The reorganization of the config, as well as introduction of grid-based tables has probably screwed this up.

    The paddingInsets and rowSpacing of stack- and grid-based tables should approximate that of the list-based implementation.

    opened by reedes 2
  • Hovered row needs to unhover if mouse cursor departs the table

    Hovered row needs to unhover if mouse cursor departs the table

    At present, the last hovered row remains hovered if the mouse cursor is no longer over the table.

    Ideally it will unhover if cursor is no longer over any part of table.

    This is similar to the select issue for Stacks and Grids (#34).

    bug 
    opened by reedes 1
  • Avoid jumpiness in sort indicator

    Avoid jumpiness in sort indicator

    Presently when sorting a column, the column title text is offset to accommodate the indicator.

    Ideally the indicator has opacity of 0 while the column is not under sort. This should avoid the jumpiness.

    opened by reedes 1
  • Most init() functions now auto-generated via template

    Most init() functions now auto-generated via template

    Most init() functions for the table variants were manually generated, resulting in needless complexity.

    Now they're generated using Sourcery with the AutoInit.stencil template.

    This is a step towards adding footer support.

    opened by reedes 0
  • Better spacing defaults for Grid

    Better spacing defaults for Grid

    To both give each item some vertical padding and to reduce rowSpacing to 0 (as Stack config has.)

    This makes Grid spacing more orthogonal to that of Stack.

    opened by reedes 0
  • BIG CHANGE moving header/row wrapper back outside of component

    BIG CHANGE moving header/row wrapper back outside of component

    A couple reasons:

    1- No longer ties header/row wrapping to LazyVGrid. You can use HStack or other wrapper for fields in your row.

    2- No more @ViewBuilder wrappers needed in declaring header/rows.

    3- Capability to attach row-level modifiers to your header/row. (e.g. context menu, etc.)

    README example will be reflected to show this change.

    Note that the experimental Grid-based tables will continue to be unwrapped and require the @ViewBuilder wrapper. More on that as the design settles.

    opened by reedes 0
  • For List-based table types, support header/footer outside scrolling region

    For List-based table types, support header/footer outside scrolling region

    ...so the header and/or footer can be independent of the scrolling of the rows. Currently they are inside the scrolling region for these table types, because otherwise there are issues with alignment in the layout.

    Header/footer should be independently configurable. See the commented code for Stack- and Grid-based tables. Move the config to the TablerConfig base class so it can be shared by all the table types.

    Any conditional views (bounded by if blocks) need to be tested and analyzed to ensure they don't affect scalability.

    enhancement 
    opened by reedes 0
  • For Stack- and Grid-based table types, support header/footer inside scrolling region

    For Stack- and Grid-based table types, support header/footer inside scrolling region

    ...so the header and/or footer can scroll with the rows. Currently they are outside the scrolling region for these table types.

    Header/footer should be independently configurable. See the commented code.

    The conditional views (bounded by if blocks) need to be tested and analyzed to ensure they don't affect scalability.

    enhancement 
    opened by reedes 0
  • Design of filtering for bound value tables not scalable

    Design of filtering for bound value tables not scalable

    Applies to tables with bound values, where an if {} is used in the ViewBuilder to include only rows that pass the filter.

    This works fine for small row count, but wouldn't work well for larger row counts.

    (Doesn't apply to Core Data, where it's assumed that the developer will apply a predicate to the FetchRequest to filter rows.)

    The README table of views notes this issue as a caveat.

    bug 
    opened by reedes 1
  • Support an alternative selection mode for Stack- and Grid-based tables

    Support an alternative selection mode for Stack- and Grid-based tables

    The current selection mode is a simple tap to select or unselect. No EditMode required on iOS. It doesn't appear to interfere with context or swipe menus.

    But ideally there would be an alternative selection mode that matches the native selection of List tables.

    On macOS, that involves discarding the selection if the user clicks in a non-row portion of the control. Selecting multiple items requires the Cmd key. Multiple rows can be selected via click and drag.

    On iOS, the native selection on List tables typically requires entering EditMode. That might be avoided altogether in the Stack- and Grid-based tables.

    enhancement 
    opened by reedes 0
Releases(0.9.6)
  • 0.9.6(May 6, 2022)

    First cut of footer support #43

    Footer use orthogonal to header use.

    As before, header/footer inside scroll region for List-based variants. And outside scroll region for Stack- and Grid-based variants.

    Configurability of header/footer in inside/outside scrolling region disabled for now, as it may have scalability issues. Will need to be investigated.

    Source code(tar.gz)
    Source code(zip)
  • 0.9.5(May 5, 2022)

    Most init() functions for the table variants were manually generated, resulting in needless complexity.

    Now they're generated using Sourcery with the AutoInit.stencil template.

    This is a step towards adding footer support #43.

    Source code(tar.gz)
    Source code(zip)
  • 0.9.4(Apr 23, 2022)

  • 0.9.3(Mar 8, 2022)

  • 0.9.2(Mar 7, 2022)

    New, cleaner hover implementation, where developer handles the hover event, giving them much more flexibility in defining look and behavior. README updated with basic example. Demo apps have more sophisticated examples.

    Rectangular area defined for SortTabler.columnTitle, for better tapGesture behavior.

    Only Grid-based tables have required config. Fixed bug where other types were still requiring one.

    Source code(tar.gz)
    Source code(zip)
  • 0.9.1(Mar 7, 2022)

    Documented config in README, along with other improvements.

    Added overlay support on tables that were missing it.

    For Stack- and Grid-based tables:

    • six new table variants to support multi-select
    • both single- and multi-select use a simple tap to select/unselect
    • does not appear to interfere with context or swipe menus

    As mentioned earlier, this library will stay in 0.9.x for a few weeks to identify and address any defects or design flaws.

    Source code(tar.gz)
    Source code(zip)
  • 0.9.0(Mar 6, 2022)

    Table views supporting reference types (class objects) no longer tied to Core Data, though they still work fine with Core Data.

    Removed the stability warning banner. Now that initial development is nearly complete, versioning will remain in 0.9.x for at least a few weeks to work out any additional bugs or design flaws.

    Source code(tar.gz)
    Source code(zip)
  • 0.8.6(Mar 6, 2022)

    • Refactored config structure
    • New grid-based variants
    • Select Overlay replaced by more general Row Overlay, available to all variants
    • Improved README
    Source code(tar.gz)
    Source code(zip)
  • 0.8.5(Mar 5, 2022)

  • 0.7.6(Mar 3, 2022)

    Reconsidered consolidation of config after it appears that it didn't provide any significant benefit.

    Table initialization now more concise.

    Source code(tar.gz)
    Source code(zip)
  • 0.7.5(Mar 3, 2022)

    Two major changes:

    #12 Consolidated configs

    #17 Headers and rows no longer wrapped by Tabler -- you must do so yourself with LazyVGrid, HStack, etc. See updated example.

    Hopefully this is the last major change prior to a 1.0 release.

    Source code(tar.gz)
    Source code(zip)
  • 0.5.0(Feb 26, 2022)

Owner
OpenAlloc
A family of libraries and tools, written in Swift, both general-purpose and with an special emphasis on investing and finance
OpenAlloc
โฌ†๏ธ A SwiftUI view component sliding in from bottom

โฌ†๏ธ A SwiftUI view component sliding in from bottom

Tieda 595 Dec 28, 2022
SwiftCrossUI - A cross-platform SwiftUI-like UI framework built on SwiftGtk.

SwiftCrossUI A SwiftUI-like framework for creating cross-platform apps in Swift. It uses SwiftGtk as its backend. This package is still quite a work-i

null 97 Dec 28, 2022
AGCircularPicker is helpful component for creating a controller aimed to manage any calculated parameter

We are pleased to offer you our new free lightweight plugin named AGCircularPicker. AGCircularPicker is helpful for creating a controller aimed to man

Agilie Team 617 Dec 19, 2022
UI Component. This is a copy swipe-panel from app: Apple Maps, Stocks. Swift version

ContainerController UI Component. This is a copy swipe-panel from app: https://www.apple.com/ios/maps/ Preview Requirements Installation CocoaPods Swi

Rustam 419 Dec 12, 2022
An easy to use UI component to help display a signal bar with an added customizable fill animation

TZSignalStrengthView for iOS Introduction TZSignalStrengthView is an easy to use UI component to help display a signal bar with an added customizable

TrianglZ LLC 22 May 14, 2022
UIPheonix is a super easy, flexible, dynamic and highly scalable UI framework + concept for building reusable component/control-driven apps for macOS, iOS and tvOS

UIPheonix is a super easy, flexible, dynamic and highly scalable UI framework + concept for building reusable component/control-driven apps for macOS, iOS and tvOS

Mohsan Khan 29 Sep 9, 2022
A panel component similar to the iOS Airpod battery panel or the Share Wi-Fi password panel.

A SwiftUI panel component similar to the iOS Airpod battery panel or the Share Wi-Fi password panel.

Red Davis 12 Feb 7, 2022
Customizable CheckBox / RadioButton component for iOS

GDCheckbox An easy to use CheckBox/Radio button component for iOS, with Attributes inspector support. Requirements Xcode 10+ Swift 5 iOS 9+ Installati

Saeid 23 Oct 8, 2022
TSnackBarView is a simple and flexible UI component fully written in Swift

TSnackBarView is a simple and flexible UI component fully written in Swift. TSnackBarView helps you to show snackbar easily with 3 styles: normal, successful and error

Nguyen Duc Thinh 3 Aug 22, 2022
TDetailBoxView is a simple and flexible UI component fully written in Swift

TDetailBoxView is a simple and flexible UI component fully written in Swift. TDetailBoxView is developed to help users quickly display the detail screen without having to develop from scratch.

Nguyen Duc Thinh 2 Aug 18, 2022
TSwitchLabel is a simple and flexible UI component fully written in Swift.

TSwitchLabel is a simple and flexible UI component fully written in Swift. TSwitchLabel is developed for you to easily use when you need to design a UI with Label and Switch in the fastest way without having to spend time on develop from scratch.

Nguyen Duc Thinh 2 Aug 18, 2022
List tree data souce to display hierachical data structures in lists-like way. It's UI agnostic, just like view-model and doesn't depend on UI framework

SwiftListTreeDataSource List tree data souce to display hierachical data structures in lists-like way. It's UI agnostic, just like view-model, so can

Dzmitry Antonenka 26 Nov 26, 2022
Create SwiftUI Views with any data

Create SwiftUI Views with any data

Zach Eriksen 20 Jun 27, 2022
Super awesome Swift minion for Core Data (iOS, macOS, tvOS)

โš ๏ธ Since this repository is going to be archived soon, I suggest migrating to NSPersistentContainer instead (available since iOS 10). For other conven

Marko Tadiฤ‡ 306 Sep 23, 2022
A fancy hexagonal layout for displaying data like your Apple Watch

Hexacon is a new way to display content in your app like the Apple Watch SpringBoard Highly inspired by the work of lmmenge. Special thanks to zenly f

Gautier Gรฉdoux 340 Dec 4, 2022
Beautiful animated placeholders for showing loading of data

KALoader Create breautiful animated placeholders for showing loading of data. You can change colors like you want. Swift 4 compatible. Usage To add an

Kirill Avery 105 May 2, 2022
Basic iOS app to track stocks (data from Finnhub, Tiingo, or IEX Cloud)

Basic iOS app to track stocks (data from Finnhub, Tiingo, or IEX Cloud)

null 33 Dec 21, 2022
A paging scroll view for SwiftUI, using internal SwiftUI components

PagingView A paging scroll view for SwiftUI, using internal SwiftUI components. This is basically the same as TabView in the paging mode with the inde

Eric Lewis 18 Dec 25, 2022
SwiftUI-Drawer - A bottom-up drawer in swiftUI

SwiftUI-Drawer A bottom-up drawer view. Contents Installation Examples Installat

Bruno Wide 9 Dec 29, 2022