Skip to content

Instantly share code, notes, and snippets.

@mayoff
Created November 5, 2021 16:28
Show Gist options
  • Save mayoff/260e019f307dfcb55f00e40a227a4225 to your computer and use it in GitHub Desktop.
Save mayoff/260e019f307dfcb55f00e40a227a4225 to your computer and use it in GitHub Desktop.
demo code for laying out a SwiftUI table with fitted columns
import SwiftUI
fileprivate struct WidthPreferenceKey: PreferenceKey {
static var defaultValue: [AnyHashable: CGFloat] { [:] }
static func reduce(value: inout [AnyHashable : CGFloat], nextValue: () -> [AnyHashable : CGFloat]) {
value.merge(nextValue(), uniquingKeysWith: { max($0, $1) })
}
}
extension WidthPreferenceKey: EnvironmentKey { }
extension EnvironmentValues {
fileprivate var widthPreference: WidthPreferenceKey.Value {
get { self[WidthPreferenceKey.self] }
set { self[WidthPreferenceKey.self] = newValue }
}
}
extension View {
public func widthPreference<Domain: Hashable>(_ key: Domain) -> some View {
return self.modifier(WidthPreferenceModifier(key: key))
}
}
fileprivate struct WidthPreferenceModifier: ViewModifier {
@Environment(\.widthPreference) var widthPreference
var key: AnyHashable
func body(content: Content) -> some View {
content
.fixedSize(horizontal: true, vertical: false)
.background(GeometryReader { proxy in
Color.clear
.preference(key: WidthPreferenceKey.self, value: [key: proxy.size.width])
})
.frame(width: widthPreference[key])
}
}
public struct WidthPreferenceDomain<Content: View>: View {
@State private var widthPreference: WidthPreferenceKey.Value = [:]
private var content: Content
public init(@ViewBuilder content: () -> Content) {
self.content = content()
}
public var body: some View {
content
.environment(\.widthPreference, widthPreference)
.onPreferenceChange(WidthPreferenceKey.self) { widthPreference = $0 }
}
}
struct ContentView: View {
var body: some View {
VStack(spacing: 0) {
ZStack {
TitleBackground()
Text("Clients")
.foregroundColor(.white)
}
TableView()
Spacer()
}
}
}
enum ColumnId: Hashable {
case name
case balance
case currency
}
struct TableView: View {
var body: some View {
WidthPreferenceDomain {
VStack(spacing: 0) {
HStack(spacing: 30) {
Text("Name")
.widthPreference(ColumnId.name)
Text("Balance")
.widthPreference(ColumnId.balance)
Text("Currency")
.widthPreference(ColumnId.currency)
Spacer()
}
.frame(maxWidth:.infinity)
.padding(12)
.background(Color(#colorLiteral(red: 0.9332349896, green: 0.9333916306, blue: 0.9332130551, alpha: 1)))
ForEach(0 ..< 4) { _ in
RowView()
}
}
}
}
}
struct RowView : View {
var body: some View {
HStack(spacing: 30) {
Text("John")
.widthPreference(ColumnId.name)
Text("$5300")
.widthPreference(ColumnId.balance)
Text("EUR")
.widthPreference(ColumnId.currency)
Spacer()
}
.padding(12)
}
}
struct TitleBackground: View {
var body: some View {
ZStack(alignment: .bottom){
Rectangle()
.frame(maxWidth: .infinity, maxHeight: 60)
.foregroundColor(.red)
.cornerRadius(15)
//We only need the top corners rounded, so we embed another rectangle to the bottom.
Rectangle()
.frame(maxWidth: .infinity, maxHeight: 15)
.foregroundColor(.red)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
@mirkokg
Copy link

mirkokg commented Jul 30, 2024

It works, but few suggestions

  1. I would remove:
    .fixedSize(horizontal: true, vertical: false)

that would make columns freely break into multiple lines.

  1. I would add alignment as an optional parameter as sometimes you need leading alignment or trailing:
extension View {
    public func widthPreference<Domain: Hashable>(_ key: Domain, alignment: Alignment = .center) -> some View {
        modifier(WidthPreferenceModifier(key: key, alignment: alignment))
    }
}

fileprivate struct WidthPreferenceModifier: ViewModifier {
    @Environment(\.widthPreference) var widthPreference
    
    var key: AnyHashable
    
    var alignment: Alignment
    
    func body(content: Content) -> some View {
        content
            .background {
                GeometryReader { proxy in
                    Color.clear
                        .preference(key: WidthPreferenceKey.self, value: [key: proxy.size.width])
                }
            }
            .frame(width: widthPreference[key], alignment: alignment)
    }
}
  1. One issue that I encountered while testing is what to do when data changes? If data changes layout won't always update? How to fix this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment