Skip to content

Instantly share code, notes, and snippets.

@rayfix
Created November 14, 2020 20:50
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save rayfix/1805ad584f1028b3e52ecf45c3f0ce9e to your computer and use it in GitHub Desktop.
Save rayfix/1805ad584f1028b3e52ecf45c3f0ce9e to your computer and use it in GitHub Desktop.
A SwiftUI 2D Grid
//
// ContentView.swift
// DataGrid
//
// Created by Ray Fix on 11/14/20.
//
import SwiftUI
struct DataGrid<Element> {
private(set) var originName: String
private(set) var rowNames: [String]
private(set) var columnNames: [String]
private var data: [[Element?]]
init(originName: String = "", rowNames: [String], columnNames: [String], repeating value: Element? = nil) {
self.originName = originName
self.rowNames = rowNames
self.columnNames = columnNames
self.data = rowNames.indices.map { _ in
columnNames.indices.map { _ in
value
}
}
}
func isContained(row: Int, column: Int) -> Bool {
rowNames.indices.contains(row) && columnNames.indices.contains(column)
}
subscript(row row: Int, column column: Int) -> Element? {
get {
guard isContained(row: row, column: column) else {
return nil
}
return data[row][column]
}
set {
data[row][column] = newValue
}
}
static func test(rows: Int, columns: Int) -> DataGrid<String> {
var dataGrid = DataGrid<String>(
rowNames: (1...rows).map { "Row \($0)" },
columnNames: (1...columns).map { "Column \($0)" } )
// Add some random data
(0..<rows).forEach { row in
(0..<columns).forEach { column in
dataGrid[row: row, column: column] = String(Double.random(in: 0...1))
}
}
//dataGrid[row: 10, column: 4] = nil
return dataGrid
}
}
struct MaxColumnWidthKey: PreferenceKey {
static var defaultValue: [Int: CGFloat] = [:]
static func reduce(value: inout [Int : CGFloat], nextValue: () -> [Int : CGFloat]) {
for (column, width) in nextValue() {
if (value[column] ?? 0) < width {
value[column] = width
}
}
}
}
struct DataGridView: View {
var dataGrid = DataGrid<String>.test(rows: 100, columns: 10)
@State private var columnWidths: [Int: CGFloat] = [:]
private func dataCell(_ string: String, column: Int) -> some View {
Text(string)
.padding()
.background(GeometryReader { proxy in
Color.clear.preference(key: MaxColumnWidthKey.self, value: [column: proxy.size.width])
})
.frame(width: columnWidths[column], height: 30, alignment: .leading)
.border(Color.black, width: 0.5)
}
private func headerCell(_ string: String, column: Int) -> some View {
dataCell(string, column: column)
.foregroundColor(.white)
.background(Color.gray)
}
var body: some View {
ScrollView([.vertical, .horizontal], showsIndicators: /*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/) {
HStack(alignment: .center, spacing: 0) {
VStack(spacing: 0) {
headerCell(dataGrid.originName, column: 0)
ForEach(Array(dataGrid.rowNames.enumerated()), id: \.offset) { offset, name in
headerCell(name, column: 0)
}
}
ForEach(dataGrid.columnNames.indices, id: \.self) { columnIndex in
VStack(spacing: 0) {
headerCell(dataGrid.columnNames[columnIndex], column: columnIndex+1)
ForEach(dataGrid.rowNames.indices, id: \.self) { rowIndex in
dataCell(dataGrid[row: rowIndex, column: columnIndex] ?? "NA",
column: columnIndex+1)
}
}
}
}
.onPreferenceChange(MaxColumnWidthKey.self) {
columnWidths = $0
}
}
}
}
struct ContentView: View {
var body: some View {
DataGridView()
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment