I'm trying to create a circular achievement view similar to the one seen in Apple Game Center using SwiftUI. The view should have circular text at the top and a centered arc wrapper text.
Here's what I'm aiming to achieve:
Circular Text: Text arranged in a circular manner at the top of the view. Arc Title: A title that wraps around an arc, centered within the circular view. I've tried using ZStack and Text views with rotation, but I'm having trouble getting the text to align correctly in a circular path. Additionally, I'm not sure how to position the arc title properly.
I'm trying to create a circular achievement view similar to the one seen in Apple Game Center using SwiftUI. The view should have circular text at the top and a centered arc wrapper text.
Here's what I'm aiming to achieve:
Circular Text: Text arranged in a circular manner at the top of the view. Arc Title: A title that wraps around an arc, centered within the circular view. I've tried using ZStack and Text views with rotation, but I'm having trouble getting the text to align correctly in a circular path. Additionally, I'm not sure how to position the arc title properly.
Share asked Mar 4 at 11:31 CodelabyCodelaby 2,9512 gold badges28 silver badges27 bronze badges 1 |2 Answers
Reset to default 2A SwiftUI solution for curved text can be found in the answer to SwiftUI: How to have equal spacing between letters in a curved text view? (it was my answer). Actually, it looks like you found this post already because your own answer here seems to be based on the code in that question (or the follow-up questions from the same OP). But you're not using CurvedText
in your answer.
The remainder of the badge can be built up with a ZStack
.
To create the gap in the circle where the title is shown, use
.trim
to shorten the path, then.rotationEffect
to move the gap into the 12 o' clock position.The size of the gap is the only slightly tricky part. If you don't like using a fixed arc fraction, you could try using an approximation of the angle based on the size of the label, or you need to get the actual arc angle of the text.
The arc angle is known inside the view
CurvedText
, so you might want to consider adapting that view. See How to determine the angle of the first character in a curved text view in SwiftUI? for an example of where a similar adaption is being used.Finally, the curved text can be added to the
ZStack
as an overlay. This way, it doesn't impact the size of theZStack
.
ZStack {
Circle()
.trim(from: 0.1, to: 0.9)
.stroke(style: .init(lineWidth: 6, lineCap: .round))
.rotationEffect(.degrees(-90))
.padding(10)
Image(.image3)
.resizable()
.scaledToFill()
.clipShape(.circle)
.padding(40)
Circle()
.stroke(lineWidth: 6)
.padding(40)
}
.frame(width: 300, height: 300)
.overlay(alignment: .top) {
// See https://stackoverflow/a/77280669/20386264
CurvedText(string: "March 4 2025", radius: 140)
.font(.title3)
.fontWeight(.medium)
}
To create a circular achievement view similar to the one seen in Apple Game Center, with circular text at the top and a centered arc wrapper title, you can use SwiftUI to build a custom view.
Create the Circular Text View
See answer @Benzy Neez: CurvedText
struct AchievementCircularView: View {
@State private var notchLenght: CGSize = .zero
var title: String
var body: some View {
GeometryReader { geometry in
let size = geometry.size
ZStack {
ArcShape(
//startAngle: .degrees(0),
length: notchLenght.width,
lineWidth: notchLenght.height,
gap: 8
)
.stroke(Color.black, style: StrokeStyle(lineWidth: 3, lineCap: .round))
.rotationEffect(.degrees(-90))
}
.frame(width: size.width, height: size.height)
.overlay(alignment: .top) {
CurvedText(text: title, radius: (size.width / 2))
.onGeometryChange(for: CGSize.self) { proxy in
proxy.size
} action: { size in
notchLenght = size
}
}
}
}
...
Create the Arc Wrapper Title
Depending on the size of the text, an arc will be drawn with the necessary notch to display the text.
private struct ArcShape: Shape {
var length: CGFloat // Length of the arc
var lineWidth: CGFloat
var gap: CGFloat = 0 // Gap to add leading and trailing spaces
func path(in rect: CGRect) -> Path {
var path = Path()
// Adjust the radius to account for the line width
let radius = min(rect.width, rect.height) / 2 - lineWidth / 2
let center = CGPoint(x: rect.midX, y: rect.midY)
let arcAngle = radius == 0 ? 0 : (length / radius)
let startAngle = -(arcAngle / 2)
// Calculate the circumference and angle ratio
let circumference = 2 * .pi * radius // Circumference of the circle
let angleRatio = length / circumference // Ratio of the length to the circumference
// Adjust the startAngle and endAngle to account for the gap
let gapAngle = Angle.radians(-(gap / circumference) * 2 * .pi)
let adjustedStartAngle = Angle.radians(startAngle) + gapAngle
let adjustedEndAngle = Angle.radians(startAngle) + .radians(angleRatio * 2 * .pi) - gapAngle
// Draw the arc
path.addArc(
center: center,
radius: radius,
startAngle: adjustedStartAngle,
endAngle: adjustedEndAngle,
clockwise: true
)
return path
}
}
Use the Circular Achievement View
You can now use the AchievementCircularView this.
let today = Date().formatted(.dateTime.day().month(.wide).year())
AchievementCircularView(title: today)
.font(.system(size: 13, design: .monospaced))
.frame(width: 200, height: 200)
//.border(.red)
.background {
Image("archivement_1")
.resizable()
.scaledToFill()
.clipShape(.circle)
.overlay {
Circle()
.stroke(.black, lineWidth: 3)
}
.padding(40)
}
func draw
. – workingdog support Ukraine Commented Mar 4 at 12:53