admin管理员组

文章数量:1410682

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
  • 1 This SO post/answer could help stackoverflow/questions/79407976/… . If you are targeting iOS18, you could also look at using TextRenderer with the func draw. – workingdog support Ukraine Commented Mar 4 at 12:53
Add a comment  | 

2 Answers 2

Reset to default 2

A 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 the ZStack.

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)
    }

本文标签: swiftuiRecreate Achievements Apple Game CenterStack Overflow