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

This is the desired outcome

enter image description here

This is what I have now

enter image description here

Can anyone help? I'm new to SwiftUI and I've been struggling for two days

The thin line and the rotation works well, but how can I keep the label horizontal at any rotation?

I have tried using a VSTack and that causes undesired behavior. And when I set the rotation only to the rectangle (thin line) I can't figure out how to correctly postion the label dynamically.

This is my code so far, and the piece at TodayLabel is where this is done

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






Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source