I would like achieve the following:
The following SwiftUI layout satisfies all but the last of these constraints. Is there any way to automatically satisfy all 4 constraints (without using a GeometryReader
but only AlignmenGuides
)?
struct AlignmentTestView: View {
var body: some View {
HStack(alignment: .top) {
VStack(alignment: .center) {
Rectangle()
.fill(.blue)
.frame(width: 50, height: 50)
Rectangle()
.fill(.green)
.frame(width: 30, height: 30)
}
VStack(alignment: .center) {
Rectangle()
.fill(.red)
.frame(width: 200, height: 40)
Rectangle()
.fill(.purple)
.frame(width: 200, height: 20)
}
}
.padding()
.background(.yellow)
}
}
(Assuming the sizes of the boxes are dynamic and can be any value)
This looks like a Grid
. Each box can be "anchored" to a point in their own grid cell.
Grid {
GridRow {
Rectangle()
.fill(.blue)
.frame(width: 50, height: 50)
.gridCellAnchor(.top)
Rectangle()
.fill(.red)
.frame(width: 200, height: 40)
.gridCellAnchor(.topLeading)
}
GridRow {
Rectangle()
.fill(.green)
.frame(width: 30, height: 30)
.gridCellAnchor(.center)
Rectangle()
.fill(.purple)
.frame(width: 200, height: 20)
.gridCellAnchor(.leading)
}
}
.padding()
.background(.yellow)
Alignment guides won't help here, because they don't work "across" containers. You can't use the alignmentGuide
+ overlay
method to lay out the views relative to one another either, because the location of each view depends on two other views. Without a custom layout like Grid
, you need to read the geometry of the views in one way or another - either using a GeometryReader
or onGeometryChange
.
If you allow the use of onGeometryChange
(not exactly GeometryReader
), then you can read the sizes of the boxes and offset
them appropriately. However, this won't affect the size of the outermost stack (the yellow area). At that point, it's easier to just use a Grid
. If you want to reinvent the wheel that is Grid
, you can put everything in a ZStack
and position
everything manually,
@State private var blueSize = CGSize.zero
@State private var redSize = CGSize.zero
@State private var greenSize = CGSize.zero
@State private var purpleSize = CGSize.zero
var body: some View {
ZStack {
Rectangle()
.fill(.blue)
.frame(width: 50, height: 50)
.onGeometryChange(for: CGSize.self, of: \.size) { blueSize = $0 }
.position(
x: max(blueSize.width, greenSize.width) / 2,
y: blueSize.height / 2
)
Rectangle()
.fill(.green)
.frame(width: 30, height: 30)
.onGeometryChange(for: CGSize.self, of: \.size) { greenSize = $0 }
.position(
x: max(blueSize.width, greenSize.width) / 2,
y: max(blueSize.height, redSize.height) + max(greenSize.height, purpleSize.height) / 2 + 8
)
Rectangle()
.fill(.red)
.frame(width: 200, height: 40)
.onGeometryChange(for: CGSize.self, of: \.size) { redSize = $0 }
.position(
x: max(blueSize.width, greenSize.width) + redSize.width / 2 + 8,
y: redSize.height / 2
)
Rectangle()
.fill(.purple)
.frame(width: 200, height: 20)
.onGeometryChange(for: CGSize.self, of: \.size) { purpleSize = $0 }
.position(
x: max(blueSize.width, greenSize.width) + purpleSize.width / 2 + 8,
y: max(blueSize.height, redSize.height) + max(greenSize.height, purpleSize.height) / 2 + 8
)
}
.frame(
width: max(blueSize.width, greenSize.width) + max(redSize.width, purpleSize.width) + 8,
height: max(blueSize.height, redSize.height) + max(greenSize.height, purpleSize.height) + 8
)
.padding()
.background(.yellow)
}
This approach works with iOS 15 or earlier, where Grid
is not available. You just need to change onGeometryChange
to a background
+ GeometryReader
+ onChange
combination.