I need to position a view based on a given CGRect
frame and a few pixels below render a RoundedRectangle
that always takes the entire width of the screen minus 20pts horizontal padding (regardless of the inner Text length). To position the first View I am using the position
modifier which seems to work just fine, but when it comes to drawing the second RoundedRectangle
and make it always take the entire width minus 20 pts horizontal padding its not working as for whatever reason the View is positioned all the way to the left and paddings ignored (check the outcome picture).
Code is simple and the following:
struct TestView: View {
let testFrame = CGRect(x: 20, y: 200, width: 240, height: 48)
var body: some View {
VStack(spacing: 20) {
RoundedRectangle(cornerRadius: 16)
.stroke(.purple, lineWidth: 4)
.frame(width: testFrame.width, height: testFrame.height)
//This should always take the entire width of the screen minus 20 pts horizontal padding and height be dynamic depending on inner Text / View sizes and lengths.
VStack(alignment: .leading) {
Text("Some title")
Text("Some subtitle")
}
.frame(maxWidth: .infinity)
.padding()
.background(RoundedRectangle(cornerRadius: 16).foregroundColor(.red))
.padding(.horizontal, 20)
}
.position(x: testFrame.midX, y: testFrame.midY)
}
}
Outcome:
The issue there, I think, is that you are trying to align the red caption "bubble" with both the purple highlight and the container. Regularly, instead of wrangling with "built-in" layouts, I would approach it with either the GeometryReader
or a custom Layout
just for mental clarity but here's both ways:
import SwiftUI
struct E25: View {
let testFrame = CGRect(x: 20, y: 200, width: 240, height: 48)
var body: some View {
RoundedRectangle(cornerRadius: 16)
.stroke(.purple, lineWidth: 4)
.frame(width: testFrame.width, height: testFrame.height)
.position(x: testFrame.midX, y: testFrame.midY)
.overlay(alignment: .top) {
VStack(alignment: .leading) {
Text("Some title")
Text("Some subtitle")
}
.frame(maxWidth: .infinity)
.padding()
.background(RoundedRectangle(cornerRadius: 16).foregroundColor(.red))
.padding(.horizontal, 20)
.offset(y: testFrame.maxY + 20)
}
}
var altBody: some View {
CustomLayout(rect: testFrame) {
RoundedRectangle(cornerRadius: 16)
.stroke(.purple, lineWidth: 4)
ZStack {
Color.red
VStack(alignment: .leading) {
Text("Some title")
Text("Some subtitle")
}
.padding()
}
.clipShape(RoundedRectangle(cornerRadius: 16))
.padding(.horizontal, 20)
}
}
struct CustomLayout: Layout {
let rect: CGRect
var captionGap: CGFloat = 20
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
// The highligh view
guard let highlight = subviews.first else { return }
highlight.place(
at: CGPoint(
x: bounds.minX + rect.minX,
y: bounds.minY + rect.minY
),
anchor: .topLeading,
proposal: ProposedViewSize(rect.size)
)
// The optional caption view
guard subviews.count >= 2 else { return }
let caption = subviews[1]
caption.place(
at: CGPoint(
x: bounds.minX,
y: bounds.minY + rect.maxY + captionGap
),
anchor: .topLeading,
proposal: ProposedViewSize(width: bounds.width, height: .none)
)
}
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
proposal.replacingUnspecifiedDimensions()
}
}
}
#Preview {
E25()
}
The first approach is a slight modification of your code but with an overlay
. Somewhat counter-intuitively, it works because position
make the modified view "greedy" (e.g. the view now takes all available space).
The second approach, (the altBody
) doesn't save any code, but, in my opinion, is somewhat easier to understand and reuse.
Let me know is this resolves the issue or if you have further questions.
Regards, –Baglan