iOS SwiftUI - 如何在标签不旋转的情况下将标签粘贴到旋转视图?

iOS SwiftUI - How to stick a label to a rotating view without the label rotating too?

这是期望的结果

这就是我现在的样子

有人能帮忙吗?初学SwiftUI,折腾了两天

细线和旋转效果很好,但如何在任何旋转时保持标签水平?

我曾尝试使用 VSTack,但它会导致意外行为。当我只将旋转设置为矩形(细线)时,我不知道如何动态地正确放置标签。

到目前为止,这是我的代码,TodayLabel 上的那篇文章就是完成此操作的地方

struct SingleRingProgressView: View {
    let startAngle: Double = 270
    let progress: Float // 0 - 1
    let ringWidth: CGFloat
    let size: CGFloat
    let trackColor: Color
    let ringColor: Color
    let centerText: AttributedText?
    let centerTextSubtitle: AttributedText?
    let todayLabel: CircleGraph.Label?

    private let maxProgress: Float = 2 // allows the ring show a progress up to 200%
    private let shadowOffsetMultiplier: CGFloat = 4

    private var absolutePercentageAngle: Float {
        percentToAngle(percent: (progress * 100), startAngle: 0)
    }

    private var relativePercentageAngle: Float {
        // Take into account the startAngle
        absolutePercentageAngle + Float(startAngle)
    }

    @State var position: (x: CGFloat, y: CGFloat) = (x: 0, y: 0)

    var body: some View {
        GeometryReader { proxy in
            HStack {
                Spacer()
                VStack {
                    Spacer()
                    ZStack {
                        Circle()
                            .stroke(lineWidth: ringWidth)
                            .foregroundColor(trackColor)
                            .frame(width: size, height: size)
                        Circle()
                            .trim(from: 0.0, to: CGFloat(min(progress, maxProgress)))
                            .stroke(style: StrokeStyle(lineWidth: ringWidth, lineCap: .round, lineJoin: .round))
                            .foregroundColor(ringColor)
                            .rotationEffect(Angle(degrees: startAngle))
                            .frame(width: size, height: size)
                        if shouldShowShadow(frame: proxy.size) {
                            Circle()
                                .fill(ringColor)
                                .frame(width: ringWidth, height: ringWidth, alignment: .center)
                                .offset(y: -(size/2))
                                .rotationEffect(Angle.degrees(360 * Double(progress)))
                                .shadow(
                                    color: Color.white,
                                    radius: 2,
                                    x: endCircleShadowOffset().0,
                                    y: endCircleShadowOffset().1)
                                .shadow(
                                    color: Color.black.opacity(0.5),
                                    radius: 1,
                                    x: endCircleShadowOffset().0,
                                    y: endCircleShadowOffset().1)

                        }
                        // Today label
                        if let todayLabel = self.todayLabel {
                            ZStack {
                                StyledText(todayLabel.label)
                                    .padding(EdgeInsets(top: 2, leading: 4, bottom: 2, trailing: 4))
                                    .background(Color.color(token: .hint))
                                    .cornerRadius(2)
                                    .offset(y: -(size/1.5))
                                Rectangle()
                                    .frame(width: 2, height: ringWidth + 2, alignment: .center)
                                    .offset(y: -(size/2))
                            }.rotationEffect(Angle.degrees(Double(todayLabel.degrees)))
                        }
                        VStack(spacing: 4) {
                            if let text = centerText {
                                StyledText(text)
                            }
                            if let subtitle = centerTextSubtitle {
                                StyledText(subtitle)
                                    .frame(maxWidth: 120)
                                    .multilineTextAlignment(.center)
                            }
                        }
                    }
                    Spacer()
                }
                Spacer()
            }
        }
    }

    private func percentToAngle(percent: Float, startAngle: Float) -> Float {
        (percent / 100 * 360) + startAngle
    }
    
    private func endCircleShadowOffset() -> (CGFloat, CGFloat) {
        let angleForOffset = absolutePercentageAngle + Float(startAngle + 90)
        let angleForOffsetInRadians = angleForOffset.toRadians()
        let relativeXOffset = cos(angleForOffsetInRadians)
        let relativeYOffset = sin(angleForOffsetInRadians)
        let xOffset = CGFloat(relativeXOffset) * shadowOffsetMultiplier
        let yOffset = CGFloat(relativeYOffset) * shadowOffsetMultiplier
        return (xOffset, yOffset)
    }

    private func shouldShowShadow(frame: CGSize) -> Bool {
        let circleRadius = min(frame.width, frame.height) / 2
        let remainingAngleInRadians = CGFloat((360 - absolutePercentageAngle).toRadians())
        if (progress * 100) >= 100 {
            return true
        } else if circleRadius * remainingAngleInRadians <= ringWidth {
            return true
        }
        return false
    }
}




只需将内部文本标签转回 -angle:

struct ContentView: View {
    let startAngle: Double = 270
    let progress: Float  = 0.2 // 0 - 1
    let ringWidth: CGFloat = 30
    let size: CGFloat = 200
    let trackColor: Color = .gray
    let ringColor: Color = .blue
    
    let todayLabeldegrees = 120.0
    
    @State var position: (x: CGFloat, y: CGFloat) = (x: 0, y: 0)
    
    var body: some View {
                ZStack {
                    Circle()
                        .stroke(lineWidth: ringWidth)
                        .foregroundColor(trackColor)
                        .frame(width: size, height: size)
                    Circle()
                        .trim(from: 0.0, to: CGFloat(progress))
                        .stroke(style: StrokeStyle(lineWidth: ringWidth, lineCap: .round, lineJoin: .round))
                        .foregroundColor(ringColor)
                        .rotationEffect(Angle(degrees: startAngle))
                        .frame(width: size, height: size)
                    
                    // Today label
                    ZStack {
                        Text("todayLabel")
                            .padding(EdgeInsets(top: 2, leading: 4, bottom: 2, trailing: 4))
                            .background(Color.white)
                            .cornerRadius(5)
                            .shadow(radius: 2)
                            .rotationEffect(Angle.degrees(-todayLabeldegrees))  // << turn back
                            .offset(y: -(size/1.5))

                        Rectangle()
                            .frame(width: 2, height: ringWidth + 2, alignment: .center)
                            .offset(y: -(size/2))
                    }
                    .rotationEffect(Angle.degrees(todayLabeldegrees))
                    
                    VStack(spacing: 4) {
                        Text("Test").font(.title)
                        Text("subtitle")
                            .frame(maxWidth: 120)
                            .multilineTextAlignment(.center)
                    }
                }
    }
}