I have the following shape drawn with some color & linewidth:
struct CustomShape: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: .zero)
path.addLine(to: CGPoint(x: rect.width, y: 0))
path.addLine(to: CGPoint(x: rect.width/2, y: rect.height))
path.closeSubpath()
return path
}
}
struct ContentView: View {
var body: some View {
ZStack {
CustomShape()
.stroke(Color.blue, lineWidth: 5)
.frame(width: 300, height: 200)
}
.ignoresSafeArea()
}
}
I'd like to draw 2 (possibly more) more rectangles inside the first one each with a different color & line width? Seems like I need the shape to conform to InsettableShape and I could then use stroke border but I'm not sure how.
I wasn't familiar with InsettableShape
, but with the help of this answer I learned how it could be used.
The difficult part of this problem is working out the position for the triangle so that the edges of an inner triangle are equally spaced from the edges of an outer triangle. This requires a bit of trigonometry!
So here are two possible solutions, both using a ZStack
to super-impose the shapes.
Shape
that you provided. A y-offset is then applied to adjust the position of the nested shapes. For this solution, the ZStack
uses alignment: .top
:struct ContentView: View {
let w: CGFloat = 250
let h: CGFloat = 220
private func yOffset(scalingFactor: CGFloat) -> CGFloat {
let dW = w - (w * scalingFactor)
let dH = h - (h * scalingFactor)
let angle = atan2(2 * h, w)
let sideSpace = (dW / 2) * sin(angle)
return (dH * sideSpace) / (dH + sideSpace)
}
private func customShape(color: Color, scalingFactor: CGFloat = 1.0) -> some View {
CustomShape()
.fill(color)
.overlay(
CustomShape()
.stroke(.black, lineWidth: 5)
)
.frame(width: w * scalingFactor, height: h * scalingFactor)
.offset(y: scalingFactor == 1.0 ? 0 : yOffset(scalingFactor: scalingFactor))
}
var body: some View {
ZStack(alignment: .top) {
customShape(color: .purple)
customShape(color: .pink, scalingFactor: 0.8)
customShape(color: .orange, scalingFactor: 0.6)
customShape(color: .yellow, scalingFactor: 0.4)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.blue)
}
}
Shape
has been converted to an InsettableShape
. The function yTop
is derived from the function yOffset
from above. The ZStack
uses default alignment this time.struct CustomShape: InsettableShape {
var insetAmount: CGFloat = 0
private func innerHeight(w: CGFloat, h: CGFloat) -> CGFloat {
let ratio = h / w
return ratio * (w - insetAmount - insetAmount)
}
private func yTop(w: CGFloat, h: CGFloat) -> CGFloat {
let dH = h - innerHeight(w: w, h: h)
let angle = atan2(2 * h, w)
let sideSpace = insetAmount * sin(angle)
return (dH * sideSpace) / (dH + sideSpace)
}
func path(in rect: CGRect) -> Path {
var path = Path()
let yTop = insetAmount == 0 ? 0 : yTop(w: rect.width, h: rect.height)
var x: CGFloat = insetAmount
var y: CGFloat = yTop
path.move(to: CGPoint(x: x, y: y))
x = rect.width - insetAmount
path.addLine(to: CGPoint(x: x, y: y))
x = rect.width / 2
y += innerHeight(w: rect.width, h: rect.height)
path.addLine(to: CGPoint(x: x, y: y))
path.closeSubpath()
return path
}
func inset(by amount: CGFloat) -> some InsettableShape {
var shape = self
shape.insetAmount += amount
return shape
}
}
struct ContentView: View {
private func customShape(color: Color, insetAmount: CGFloat = 0) -> some View {
CustomShape()
.inset(by: insetAmount)
.fill(color)
.strokeBorder(.black, lineWidth: 5)
}
var body: some View {
ZStack {
customShape(color: .purple)
customShape(color: .pink, insetAmount: 25)
customShape(color: .orange, insetAmount: 50)
customShape(color: .yellow, insetAmount: 75)
}
.frame(width: 250, height: 220)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.blue)
}
}
I would say, implementing the shape in this way is more complicated. However, the calling code (in other words, ContentView
) is now simpler and I like the way the knowledge of the shape is all contained inside the shape itself.
Both solutions give the same result: