最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

swiftui - Recreate Achievements Apple Game Center - Stack Overflow

programmeradmin0浏览0评论

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)
    }
发布评论

评论列表(0)

  1. 暂无评论