Skip to content

Instantly share code, notes, and snippets.

@rnapier
Last active September 27, 2020 20:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rnapier/25f9c58405f176e9a6a84f81f67e74c0 to your computer and use it in GitHub Desktop.
Save rnapier/25f9c58405f176e9a6a84f81f67e74c0 to your computer and use it in GitHub Desktop.
GridView that makes me cry
// 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 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