Skip to main content

Benutzerdefinierte Fortschrittsanzeige

Beschreibung

Diese SwiftUI-View zeigt eine animierte, kreisförmige Fortschrittsanzeige mit Segmenten, die sich entsprechend dem Fortschritt füllen. Die Anzeige kombiniert funktionales Feedback und ein modernes, ästhetisches Design und eignet sich ideal, um Benutzern den Verlauf von Aufgaben oder Ladeprozessen visuell darzustellen.

🔍 Zweck

  • Darstellung von Lade- oder Verarbeitungsfortschritten in Apps
  • Visualisierung des Abschlussgrads bei Trainings- oder Lernzielen
  • Anzeigen von Upload-/Download-Fortschritt in Dateimanagern
  • Fortschrittsfeedback bei mehrstufigen Onboarding-Prozessen
  • Gamification: Anzeigen von Level-Fortschritt oder Belohnungen

📄 Codebeispiel

import SwiftUI

/// A circular segmented progress indicator with animated segment fill.
struct CircularSegmentedProgressView: View {
    var progress: Double         // Progress between 0.0 and 1.0
    let segmentCount: Int        // Number of segments in the circle
    let lineWidth: CGFloat       // Thickness of the segment strokes
    let filledColor: Color       // Color for filled segments
    let emptyColor: Color        // Color for empty segments
    let animation: Animation     // Animation for progress change

    init(
        progress: Double,
        segmentCount: Int = 12,
        lineWidth: CGFloat = 14,
        filledColor: Color = .accentColor,
        emptyColor: Color = .gray.opacity(0.3),
        animation: Animation = .easeInOut(duration: 0.6)
    ) {
        self.progress = min(max(progress, 0), 1) // Clamp between 0...1
        self.segmentCount = segmentCount
        self.lineWidth = lineWidth
        self.filledColor = filledColor
        self.emptyColor = emptyColor
        self.animation = animation
    }

    var body: some View {
        GeometryReader { geo in
            ZStack {
                ForEach(0..<segmentCount, id: \.self) { index in
                    SegmentShape(
                        segmentIndex: index,
                        totalSegments: segmentCount,
                        thickness: lineWidth
                    )
                    .stroke(
                        index < filledSegments ? filledColor : emptyColor,
                        style: StrokeStyle(lineWidth: lineWidth, lineCap: .round)
                    )
                    .animation(animation, value: filledSegments)
                }
            }
            .frame(width: geo.size.width, height: geo.size.height)
            .contentShape(Circle())
            // Optional overlay for displaying percent text in the center:
            .overlay(
                Text("\(Int(progress * 100))%")
                    .font(.title2.bold())
                    .foregroundColor(.primary)
                    .accessibilityLabel(Text("Fortschritt \(Int(progress * 100)) Prozent"))
            )
        }
        .aspectRatio(1, contentMode: .fit)
        .padding(lineWidth / 2)
    }

    /// Calculates how many full segments should be filled based on current progress.
    private var filledSegments: Int {
        Int(round(progress * Double(segmentCount)))
    }
}

/// Shape representing a single arc segment.
struct SegmentShape: Shape {
    let segmentIndex: Int
    let totalSegments: Int
    let thickness: CGFloat

    func path(in rect: CGRect) -> Path {
        let radius = min(rect.width, rect.height) / 2 - thickness / 2
        let anglePerSegment = Angle.degrees(360 / Double(totalSegments))
        let startAngle = Angle.degrees(-90) + anglePerSegment * Double(segmentIndex)
        let endAngle = startAngle + anglePerSegment * 0.92 // Small gap between segments

        var path = Path()
        path.addArc(
            center: CGPoint(x: rect.midX, y: rect.midY),
            radius: radius,
            startAngle: startAngle,
            endAngle: endAngle,
            clockwise: false
        )
        return path
    }
}

// Demo view for interactive testing.
struct CircularSegmentedProgressDemoView: View {
    @State private var progressValue = 0.55

    var body: some View {
        VStack(spacing: 32) {
            CircularSegmentedProgressView(
                progress: progressValue,
                segmentCount: 16,
                lineWidth: 16,
                filledColor: .blue,
                emptyColor: .gray.opacity(0.25)
            )
            .frame(width: 180, height: 180)

            Slider(value: $progressValue, in: 0...1)
                .padding(.horizontal)
                .accentColor(.blue)

            Button("Reset") {
                withAnimation { progressValue = 0 }
            }
            .buttonStyle(.borderedProminent)
            
            Button("Random Progress") {
                withAnimation { progressValue = Double.random(in: 0...1) }
            }
            .buttonStyle(.bordered)
        }
        .padding()
    }
}

#Preview {
    CircularSegmentedProgressDemoView()
}