Last active
March 5, 2025 16:55
-
-
Save rnapier/25f9c58405f176e9a6a84f81f67e74c0 to your computer and use it in GitHub Desktop.
GridView that makes me cry
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This GridView makes me cry. It is recreating an HTML-style bordered table, sized to | |
// its data, with a header. It requires a GeometryReader and Preferences, which might | |
// be unavoidable, but it also requires a *horrible* DispatchQueue.main.async in updateMaxValue. | |
// This means it doesn't work in Previews, and completely breaks the idea of "declarative" UI. | |
import SwiftUI | |
struct WidthPreferenceKey: PreferenceKey { | |
static var defaultValue: CGFloat = .zero | |
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { | |
value = max(value, nextValue()) | |
} | |
} | |
struct HeightPreferenceKey: PreferenceKey { | |
static var defaultValue: CGFloat = .zero | |
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { | |
value = max(value, nextValue()) | |
} | |
} | |
extension View { | |
func updateMaxValue<K, Index>(_ key: K.Type = K.self, in values: Binding<[Index: K.Value]>, for n: Index) -> some View | |
where K : PreferenceKey, K.Value : Comparable | |
{ | |
return onPreferenceChange(K.self) { newValue in | |
DispatchQueue.main.async { | |
if let oldValue = values.wrappedValue[n], newValue <= oldValue { | |
// Leave old value | |
} else { | |
values[n].wrappedValue = newValue | |
} | |
} | |
} | |
} | |
} | |
struct GridRow<Cell: View>: View { | |
@Binding var widths: [Int: CGFloat] | |
@Binding var heights: [Int: CGFloat] | |
var columns: [Cell] | |
var row: Int | |
var body: some View { | |
HStack(spacing: -1) { | |
ForEach(Array(columns.enumerated()), id: \.offset) { (n, cell) in | |
cell | |
.padding(.vertical, 4) | |
.padding(.horizontal, 12) | |
.background(GeometryReader { g in | |
Color.clear | |
.transformPreference(WidthPreferenceKey.self) { width in width = max(width, g.size.width) } | |
.transformPreference(HeightPreferenceKey.self) { height in height = max(height, g.size.height) } | |
}) | |
.updateMaxValue(WidthPreferenceKey.self, in: self.$widths, for: n) | |
.updateMaxValue(HeightPreferenceKey.self, in: self.$heights, for: self.row) | |
.frame(minWidth: self.widths[n], minHeight: self.heights[self.row]) | |
.border(Color.white, width: 1.5) | |
} | |
} | |
} | |
} | |
struct GridView<Header: View, Cell: View>: View { | |
var header: [Header] | |
var rows: [[Cell]] | |
@State var widths: [Int: CGFloat] = [:] | |
@State var heights: [Int: CGFloat] = [:] | |
var body: some View { | |
VStack(spacing: -1) { | |
GridRow(widths: $widths, heights: $heights, columns: header, row: -1).font(Font.system(.headline)) | |
ForEach(Array(rows.enumerated()), id: \.offset) { (row, value) in | |
GridRow(widths: self.$widths, heights: self.$heights, columns: value, row: row) | |
} | |
} | |
} | |
} | |
struct GridView_Previews: PreviewProvider { | |
static let testHeader = ["First", "Last", "Birthday"].map(Text.init) | |
static let testRows = [ | |
[AnyView(Text("Bob")), AnyView(Text("Baker")), AnyView(Text("2000-02-02"))], | |
[AnyView(Text("Alice with a very very long name")), AnyView(Text("Adams")), AnyView(Text("2000-01-01"))] | |
] | |
static var previews: some View { | |
GridView(header: testHeader, rows: testRows) | |
.frame(width: 420) | |
.foregroundColor(.white) | |
.background(Color.gray) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This GridView makes me much, much happier. No asyncs. Not even @Bindings. Just a couple of preferences. | |
import SwiftUI | |
struct WidthsPreferenceKey: PreferenceKey { | |
static var defaultValue: [Int: CGFloat] = [:] | |
static func reduce(value: inout [Int: CGFloat], nextValue: () -> [Int: CGFloat]) { | |
value.merge(nextValue(), uniquingKeysWith: max) | |
} | |
} | |
struct HeightPreferenceKey: PreferenceKey { | |
static var defaultValue: CGFloat = .zero | |
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { | |
value = max(value, nextValue()) | |
} | |
} | |
struct GridRow<Cell: View>: View { | |
var columns: [Cell] | |
var row: Int | |
var widths: [Int: CGFloat] | |
@State var rowHeight: CGFloat = 0 | |
var body: some View { | |
HStack(spacing: -1) { | |
ForEach(Array(columns.enumerated()), id: \.offset) { (n, cell) in | |
cell | |
.fixedSize(horizontal: false, vertical: true) | |
.padding(.vertical, 4) | |
.padding(.horizontal, 12) | |
.background(GeometryReader { g in | |
Color.clear | |
.preference(key: HeightPreferenceKey.self, value: g.size.height) | |
.preference(key: WidthsPreferenceKey.self, value: [n: g.size.width]) | |
}) | |
.frame(width: widths[n], height: rowHeight) | |
.border(Color.white, width: 1.5) | |
} | |
} | |
.onPreferenceChange(HeightPreferenceKey.self) { value in | |
rowHeight = value | |
} | |
} | |
} | |
struct GridView<Header: View, Cell: View>: View { | |
var header: [Header] | |
var rows: [[Cell]] | |
@State var widths: [Int: CGFloat] = [:] | |
var body: some View { | |
VStack(spacing: -1) { | |
GridRow(columns: header, row: -1, widths: widths).font(Font.system(.headline)) | |
ForEach(Array(rows.enumerated()), id: \.offset) { (row, value) in | |
GridRow(columns: value, row: row, widths: widths) | |
} | |
} | |
.onPreferenceChange(WidthsPreferenceKey.self) { value in | |
widths = value | |
} | |
} | |
} | |
struct GridView_Previews: PreviewProvider { | |
static let testHeader = ["First", "Last", "Birthday"].map(Text.init) | |
static let testRows = [ | |
[Text("Bob"), Text("Baker"), Text("2000-02-02")], | |
[Text("Alice with a very very long name"), Text("Adams"), Text("2000-01-01")] | |
] | |
static var previews: some View { | |
Group { | |
GridView(header: testHeader, rows: testRows) | |
.frame(width: 420) | |
.foregroundColor(.white) | |
.background(Color.gray) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment