im trying to replicate a view I saw on Apple's documentation about the Dynamic Island and live activities, I managed to make the rounded arc (with some help haha) and now im trying to do the rounded pizza-like shape. Also im not sure how to add the little rotated satellite to the view
This is the view im trying to replicate:
I tried and managed to get the pizza-like shape with Path
but I don't know how to round the corners, I tried .cornerRadius
, .clipShape
and didn't work.
This is my current code:
Arc:
This was done with some help on my previous question
struct Arc: Shape {
/// Percent
///
/// How much of the Arc should we draw? `1` means 100%. `0.5` means half. Etc.
let percent: CGFloat
/// Line width
///
/// How wide is the line going to be stroked. This is used to offset the arc within the `CGRect`.
let lineWidth: CGFloat
init(percent: CGFloat = 1, lineWidth: CGFloat = 1) {
self.percent = percent
self.lineWidth = lineWidth
}
func path(in rect: CGRect) -> Path {
let rect = rect.insetBy(dx: lineWidth / 2, dy: lineWidth / 2)
var path = Path()
let x = rect.width / 2
let y = rect.height
let w = sqrt(x * x + y * y)
let phi = atan2(x, y)
let radius = w / 2 / cos(phi)
let center = CGPoint(x: rect.minX + x, y: rect.minY + radius)
let theta = 2 * (.pi / 2 - phi) * percent
let startAngle = Angle(radians: 3 * .pi / 2 - theta)
let endAngle = Angle(radians: 3 * .pi / 2 + theta)
path.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: false)
return path
}
}
RoundedTriangleShape:
struct RoundedTriangleShape : Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
let x = rect.width / 2
let y = rect.height
let w = sqrt(x * x + y * y)
let phi = atan2(x, y)
let radius = w / 2 / cos(phi)
let center = CGPoint(x: rect.minX + x, y: rect.minY + radius)
let startAngle: Angle = .degrees(250)
let endAngle: Angle = .degrees(290)
path.move(to: CGPoint(x: rect.midX, y: rect.midY + y / 1.5))
path.addArc(center: center, radius: radius - 10, startAngle: startAngle, endAngle: endAngle, clockwise: false)
path.closeSubpath()
return path
}
}
DynamicIslandExpandedRegion:
DynamicIslandExpandedRegion(.bottom) {
GeometryReader { geometry in
HStack {
Spacer()
ZStack {
Arc(lineWidth: 20)
.stroke(style: StrokeStyle(lineWidth: 20, lineCap: .round))
.foregroundStyle(.indigo.opacity(0.5))
RoundedTriangleShape()
.foregroundStyle(.linearGradient(colors: [.blue, .cyan],
startPoint: .bottom,
endPoint: .top).opacity(0.5))
Arc(percent: 0.35, lineWidth: 20)
.stroke(style: StrokeStyle(lineWidth: 20, lineCap: .round))
.foregroundStyle(.cyan)
}
.frame(width: geometry.size.width / 1.2,
height: geometry.size.width / 5, alignment: .center)
Spacer()
}
}
.frame(height: 300)
}
And the current state of the view:
Current State:
The usual trick for rounded corners would be to stroke the path with the wide lineWidth
, and then to fill the same path. But that will not work here because you are using an opacity of 0.5, so where the stroked outline and the fill overlapped, you would get greater opacity than you intended.
So, here, you need to construct a path of the outline of the shape you want. And this will consist of the top outer edge of the main arc (in red, below), an arc for the top-right corner (yellow), an arc for the bottom (black), and a final arc for the top-left corner (white).
Note, we do not need to stroke the left and right sides, as those will be rendered for us: When you have an addPath
for one arc followed by another, it will automatically include a line from the end of the previous path to the start of the next one. The trick is to have the arcs start and end at the same angle that matches the line between them:
Anyway, that is achieved with something like the following, expanding upon my prior answer:
struct ArcFilledOutline: Shape {
/// Percent
///
/// How much of the Arc should we draw? `1` means 100%. `0.5` means half. Etc.
let percent: CGFloat
/// Line width
///
/// How wide is the line going to be stroked. This is used to offset the arc within the `CGRect`.
let lineWidth: CGFloat
init(percent: CGFloat = 1, lineWidth: CGFloat = 1) {
self.percent = percent
self.lineWidth = lineWidth
}
func path(in rect: CGRect) -> Path {
let rect = rect.insetBy(dx: lineWidth / 2, dy: lineWidth / 2)
var path = Path()
let x = rect.width / 2
let y = rect.height
let w = sqrt(x * x + y * y)
let phi = atan2(x, y)
let radius = w / 2 / cos(phi)
let center = CGPoint(x: rect.minX + x, y: rect.minY + radius)
let theta = 2 * (.pi / 2 - phi) * percent
let startAngle = Angle(radians: 3 * .pi / 2 - theta)
let endAngle = Angle(radians: 3 * .pi / 2 + theta)
path.addArc(
center: center,
radius: radius + lineWidth / 2,
startAngle: startAngle,
endAngle: endAngle,
clockwise: false
)
let topRightCenter = CGPoint(x: center.x + radius * CGFloat(cos(endAngle.radians)), y: center.y + radius * CGFloat(sin(endAngle.radians)))
let topLeftCenter = CGPoint(x: center.x + radius * CGFloat(cos(startAngle.radians)), y: center.y + radius * CGFloat(sin(startAngle.radians)))
let bottomCenter = CGPoint(x: rect.midX, y: rect.maxY)
let rightAngle = Angle(radians: atan2(topRightCenter.y - bottomCenter.y, topRightCenter.x - bottomCenter.x) + .pi / 2)
let leftAngle = Angle(radians: atan2(bottomCenter.y - topLeftCenter.y, bottomCenter.x - topLeftCenter.x) + .pi / 2)
path.addArc(
center: topRightCenter,
radius: lineWidth / 2,
startAngle: endAngle,
endAngle: rightAngle,
clockwise: false
)
path.addArc(
center: bottomCenter,
radius: lineWidth / 2,
startAngle: rightAngle,
endAngle: leftAngle,
clockwise: false
)
path.addArc(
center: topLeftCenter,
radius: lineWidth / 2,
startAngle: leftAngle,
endAngle: startAngle,
clockwise: false
)
path.closeSubpath()
return path
}
}
struct Arc: Shape {
/// Percent
///
/// How much of the Arc should we draw? `1` means 100%. `0.5` means half. Etc.
let percent: CGFloat
/// Line width
///
/// How wide is the line going to be stroked. This is used to offset the arc within the `CGRect`.
let lineWidth: CGFloat
init(percent: CGFloat = 1, lineWidth: CGFloat = 1) {
self.percent = percent
self.lineWidth = lineWidth
}
func path(in rect: CGRect) -> Path {
let rect = rect.insetBy(dx: lineWidth / 2, dy: lineWidth / 2)
var path = Path()
let x = rect.width / 2
let y = rect.height
let w = sqrt(x * x + y * y)
let phi = atan2(x, y)
let radius = w / 2 / cos(phi)
let center = CGPoint(x: rect.minX + x, y: rect.minY + radius)
let theta = 2 * (.pi / 2 - phi) * percent
let startAngle = Angle(radians: 3 * .pi / 2 - theta)
let endAngle = Angle(radians: 3 * .pi / 2 + theta)
path.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: false)
return path
}
}
And needless to say, rather than stroking the outline path, like I did above, you would just fill
as desired. E.g.:
struct ContentView: View {
var body: some View {
GeometryReader { geometry in
ZStack {
Arc(lineWidth: 20)
.stroke(style: StrokeStyle(lineWidth: 20, lineCap: .round))
.foregroundStyle(Color.blue)
.frame(width: geometry.size.width, height: geometry.size.width / 4, alignment: .center)
ArcFilledOutline(percent: 0.33, lineWidth: 20)
.fill()
.foregroundStyle(
.linearGradient(
colors: [.blue, .cyan],
startPoint: .bottom,
endPoint: .top
).opacity(0.5)
)
.frame(width: geometry.size.width, height: geometry.size.width / 4, alignment: .center)
Arc(percent: 0.33, lineWidth: 20)
.stroke(style: StrokeStyle(lineWidth: 20, lineCap: .round))
.foregroundStyle(Color.cyan)
.frame(width: geometry.size.width, height: geometry.size.width / 4, alignment: .center)
}
}
.padding()
}
}
Yielding: