swiftswiftui

SwiftUI concentric circles that show progress by gradually appearing


I have a game with three levels and each level is 5 points (15 points in total). I'd like to show user progress with three concentric circles or rings, representing each level. The rings start empty, with just an outline, and as the user collects a point, one fifth of the circle gets filled with a color, until the full circle is filled, at which point, the process repeats with the next level. I have the below code, but for some reason, when I run it through the simulator, the circles start off filled already. How can I make them gradually fill (one fifth at a time) as the user collects points?

import SwiftUI

struct ContentView: View {
    @State private var progressLevel1: CGFloat = 0.0
    @State private var progressLevel2: CGFloat = 0.0
    @State private var progressLevel3: CGFloat = 0.0

    var body: some View {
        VStack {
            Spacer()
            
            ZStack {
                Circle()
                    .stroke(Color.green, lineWidth: 20)
                    .frame(width: 150, height: 150)
                    .overlay(
                        Circle()
                            .trim(from: 0.0, to: progressLevel1)
                            .stroke(Color.green, lineWidth: 20)
                            .frame(width: 150, height: 150)
                            .rotationEffect(.degrees(-90))
                    )
                
                Circle()
                    .stroke(Color.blue, lineWidth: 20)
                    .frame(width: 200, height: 200)
                    .overlay(
                        Circle()
                            .trim(from: 0.0, to: progressLevel2)
                            .stroke(Color.blue, lineWidth: 20)
                            .frame(width: 200, height: 200)
                            .rotationEffect(.degrees(-90))
                    )
                
                Circle()
                    .stroke(Color.purple, lineWidth: 20)
                    .frame(width: 250, height: 250)
                    .overlay(
                        Circle()
                            .trim(from: 0.0, to: progressLevel3)
                            .stroke(Color.purple, lineWidth: 20)
                            .frame(width: 250, height: 250)
                            .rotationEffect(.degrees(-90))
                    )
            }
            
            Spacer()
            
            Button("Collect Point") {
                withAnimation {
                    if progressLevel1 < 1.0 {
                        progressLevel1 += 0.2
                    } else if progressLevel2 < 1.0 {
                        progressLevel2 += 0.2
                    } else if progressLevel3 < 1.0 {
                        progressLevel3 += 0.2
                    }
                }
            }
            .padding()
        }
        .onAppear {
            // Set initial values to 0 to start with empty circles
            progressLevel1 = 0.0
            progressLevel2 = 0.0
            progressLevel3 = 0.0
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}


Solution

  • Use this approach, if you want to initially show a faint outline:

     Circle().stroke(Color.green.opacity(0.1), lineWidth: 20)
    

    Similarly for the other colors (not the Circles in the overlay). Adjust the opacity to your liking.

    EDIT-1:

    Here is my full test code. It shows the progress by gradually filling the concentric circles or rings representing each level, with different colors.

    On MacOS 14.2, using Xcode 15.1, tested on real ios17 devices (not Previews) and MacCatalyst.

    struct ContentView: View {
        @State private var progressLevel1: CGFloat = 0.0
        @State private var progressLevel2: CGFloat = 0.0
        @State private var progressLevel3: CGFloat = 0.0
    
        var body: some View {
            VStack {
                Spacer()
                
                ZStack {
                    Circle()
                        .stroke(Color.green.opacity(0.1), lineWidth: 20) // <-- here
                        .frame(width: 150, height: 150)
                        .overlay(
                            Circle()
                                .trim(from: 0.0, to: progressLevel1)
                                .stroke(Color.green, lineWidth: 20)
                                .frame(width: 150, height: 150)
                                .rotationEffect(.degrees(-90))
                        )
                    
                    Circle()
                        .stroke(Color.blue.opacity(0.1), lineWidth: 20) // <-- here
                        .frame(width: 200, height: 200)
                        .overlay(
                            Circle()
                                .trim(from: 0.0, to: progressLevel2)
                                .stroke(Color.blue, lineWidth: 20)
                                .frame(width: 200, height: 200)
                                .rotationEffect(.degrees(-90))
                        )
                    
                    Circle()
                        .stroke(Color.purple.opacity(0.1), lineWidth: 20) // <-- here
                        .frame(width: 250, height: 250)
                        .overlay(
                            Circle()
                                .trim(from: 0.0, to: progressLevel3)
                                .stroke(Color.purple, lineWidth: 20)
                                .frame(width: 250, height: 250)
                                .rotationEffect(.degrees(-90))
                        )
                }
                
                Spacer()
                
                Button("Collect Point") {
                    withAnimation {
                        if progressLevel1 < 1.0 {
                            progressLevel1 += 0.2
                        } else if progressLevel2 < 1.0 {
                            progressLevel2 += 0.2
                        } else if progressLevel3 < 1.0 {
                            progressLevel3 += 0.2
                        }
                    }
                }
                .padding()
            }
            .onAppear {
                // Set initial values to 0 to start with empty circles
                progressLevel1 = 0.0
                progressLevel2 = 0.0
                progressLevel3 = 0.0
            }
        }
    }