Weighted HStack and VStack in SwiftUI
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
// Full recipe at https://swiftuirecipes.com/blog/weighted-layout-hstack-and-vstack-in-swiftui | |
import SwiftUI | |
class WeightedProxy { | |
let kind: Kind | |
var geo: GeometryProxy? = nil | |
private(set) var totalWeight: CGFloat = 0 | |
init(kind: Kind) { | |
self.kind = kind | |
} | |
func register(with weight: CGFloat) { | |
totalWeight += weight | |
} | |
func dimensionForRelative(weight: CGFloat) -> CGFloat { | |
guard let geo = geo, | |
totalWeight > 0 | |
else { | |
return 0 | |
} | |
let dimension = (kind == .vertical) ? geo.size.height : geo.size.width | |
return dimension * weight / totalWeight | |
} | |
enum Kind { | |
case vertical, horizontal | |
} | |
} | |
struct Weighted: ViewModifier { | |
private let weight: CGFloat | |
private let proxy: WeightedProxy | |
init(_ weight: CGFloat, proxy: WeightedProxy) { | |
self.weight = weight | |
self.proxy = proxy | |
proxy.register(with: weight) | |
} | |
@ViewBuilder func body(content: Content) -> some View { | |
if proxy.kind == .vertical { | |
content.frame(height: proxy.dimensionForRelative(weight: weight)) | |
} else { | |
content.frame(width: proxy.dimensionForRelative(weight: weight)) | |
} | |
} | |
} | |
extension View { | |
func weighted(_ weight: CGFloat, proxy: WeightedProxy) -> some View { | |
self.modifier(Weighted(weight, proxy: proxy)) | |
} | |
} | |
struct WeightedHStack<Content>: View where Content : View { | |
private let proxy = WeightedProxy(kind: .horizontal) | |
@State private var initialized = false | |
@ViewBuilder let content: (WeightedProxy) -> Content | |
var body: some View { | |
GeometryReader { geo in | |
HStack(spacing: 0) { | |
if initialized { | |
content(proxy) | |
} else { | |
Color.clear.onAppear { | |
proxy.geo = geo | |
initialized.toggle() | |
} | |
} | |
} | |
} | |
} | |
} | |
struct WeightedVStack<Content>: View where Content : View { | |
private let proxy = WeightedProxy(kind: .vertical) | |
@State private var initialized = false | |
@ViewBuilder let content: (WeightedProxy) -> Content | |
var body: some View { | |
GeometryReader { geo in | |
VStack(spacing: 0) { | |
if initialized { | |
content(proxy) | |
} else { | |
Color.clear.onAppear { | |
proxy.geo = geo | |
initialized.toggle() | |
} | |
} | |
} | |
} | |
} | |
} | |
struct WeightsTest: View { | |
var body: some View { | |
// VStack { | |
// WeightedHStack { proxy in | |
// Text("50%") | |
// .weighted(5, proxy: proxy) | |
// .background(Color.blue) | |
// Text("20%") | |
// .weighted(2, proxy: proxy) | |
// .background(Color.green) | |
// Text("30%") | |
// .weighted(3, proxy: proxy) | |
// .background(Color.red) | |
// } | |
// WeightedHStack { proxy in | |
// Text("15%") | |
// .weighted(0.15, proxy: proxy) | |
// .background(Color.brown) | |
// Text("15%") | |
// .weighted(0.15, proxy: proxy) | |
// .background(Color.cyan) | |
// Text("55%") | |
// .weighted(0.55, proxy: proxy) | |
// .background(Color.black) | |
// Text("15%") | |
// .weighted(0.15, proxy: proxy) | |
// .background(Color.pink) | |
// } | |
// WeightedHStack { proxy in | |
// Text("30%") | |
// .weighted(3, proxy: proxy) | |
// .background(Color.purple) | |
// Text("40%") | |
// .weighted(4, proxy: proxy) | |
// .background(Color.yellow) | |
// Text("30%") | |
// .weighted(3, proxy: proxy) | |
// .background(Color.indigo) | |
// } | |
// Spacer() | |
// } | |
WeightedVStack { proxy in | |
Text("20%") | |
.frame(minWidth: 0, maxWidth: .infinity) | |
.weighted(2, proxy: proxy) | |
.background(Color.green) | |
Text("50%") | |
.frame(minWidth: 0, maxWidth: .infinity) | |
.weighted(5, proxy: proxy) | |
.background(Color.red) | |
Text("30%") | |
.frame(minWidth: 0, maxWidth: .infinity) | |
.weighted(3, proxy: proxy) | |
.background(Color.cyan) | |
} | |
.padding() | |
.foregroundColor(.white) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment