Using SwiftUI, I have a rectangle that takes up a percentage of the device's width, with an inner rectangle in an overlay.
struct ContentView: View {
var body: some View {
Rectangle()
.fill(.purple)
.containerRelativeFrame([.horizontal, .vertical]) { myWidth, axis in
if axis == .horizontal {
return myWidth * 0.8
} else {
return 40.0
}
}
.overlay(CandyView(theWidth: 0.25), alignment: .leading)
.cornerRadius(8.0)
}
}
struct CandyView: View {
var theWidth: CGFloat
var body: some View {
Rectangle()
.fill(.white)
.containerRelativeFrame([.horizontal, .vertical]) { myWidth, axis in
if axis == .horizontal {
return myWidth * theWidth
} else {
return 35.0
}
}
.cornerRadius(7.0)
.padding([.leading, .trailing], 3.0)
}
}
This will give me something like this:
I can place the inner rectangle on the other side, or in the middle, by changing the alignment:
.overlay(CandyView(theWidth: 0.25), alignment: .leading)
But what if I wanted to place the inner rectangle 20% along the outer rectangle? Is there an easy way to specify a percentage to place it where I'd like it to be? I'd prefer not to use GeometryReader
, but I'm open to suggestions.
You could always change the overlay to an HStack
containing a spacer + the marker shape.
You might notice, that the white marker in your example has a width that is 25% of the screen, not 25% of the underlying view. This is because, the reference container for .containerRelativeFrame
is actually the screen in this case. See the documentation for more details.
ā To correct the width, the base fraction needs to be applied to all overlay fractions too.
Btw, the .overlay
modifier you are using is deprecated, suggest using overlay(alignment:content:)
instead. Also, .cornerRadius
is deprecated, suggest applying a clip shape instead, or just use a RoundedRectangle
as the base shape:
private let baseWidthFraction: CGFloat = 0.8
RoundedRectangle(cornerRadius: 8)
.fill(.purple)
.containerRelativeFrame([.horizontal, .vertical]) { width, axis in
axis == .horizontal ? width * baseWidthFraction : 40.0
}
.overlay(alignment: .leading) {
HStack(spacing: 0) {
Color.clear
.containerRelativeFrame(.horizontal) { width, axis in
width * 0.2 * baseWidthFraction
}
CandyView(theWidth: 0.25 * baseWidthFraction)
}
}
An alternative way to split the view is to use a custom Layout
. For the case here, there is only one child to position, so the layout implementation can be greatly simplified:
struct FractionalLayout: Layout {
var spacerFraction: CGFloat = 0.0
let contentFraction: CGFloat
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
proposal.replacingUnspecifiedDimensions()
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
if let child = subviews.first {
let x = bounds.minX + (bounds.width * spacerFraction)
let w = bounds.width * contentFraction
child.place(
at: CGPoint(x: x, y: bounds.minY),
proposal: .init(width: w, height: bounds.height)
)
}
}
}
Example use:
.overlay {
CandyView(spacerWidth: 0.2, theWidth: 0.25)
}
struct CandyView: View {
var spacerWidth = CGFloat.zero
let theWidth: CGFloat
var body: some View {
FractionalLayout(spacerFraction: spacerWidth, contentFraction: theWidth) {
RoundedRectangle(cornerRadius: 7)
.fill(.white)
}
.padding(.horizontal, 3)
.padding(.vertical, 2.5)
}
}
The result is very similar to the previous result, except that the width of the marker is now determined after allowing for the horizontal padding, which means it is actually slightly more precise than before:
For a more generic Layout
implementation that supports multiple subviews, see the answer to SwiftUI How to Set Specific Width Ratios for Child Elements in an HStack (it was my answer). In that implementation, the available width is split according to the weights given to the subviews. Here's how it could be used to give the same result here:
.overlay {
WeightedHStack {
Color.clear.layoutWeight(20)
RoundedRectangle(cornerRadius: 7)
.fill(.white)
.frame(height: 35)
.layoutWeight(25)
Color.clear.layoutWeight(55)
}
.padding(.horizontal, 3)
}