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

Time:02-02

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




CodePudding user response:

just turn the inner text label back by -angle:

enter image description here

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)
                    }
                }
    }
}
  •  Tags:  
  • Related