9
votes

I've been experimenting with some SwiftUI layouts and one of the things that I wanted to try out was creating a simple circular progress ring. After playing around with the code for a while I managed to get everything working the way I was hoping for it to, at least for a prototype. The issue arrises when I embed this view inside a SwiftUI NavigationView. Now, every time I run the app in the canvas, simulator, or on a device, the initial loading of the progress ring has the entire view slowly sliding up into position.

This is a simple prototype, just messing around with the new SwiftUI tools. After some experimentation, I've found that if I remove the NavigationView the ring acts like it's meant to from the beginning. I'm not seeing an obvious reason for why this issue is occurring though.

import SwiftUI

struct ProgressRing_ContentView: View {

@State var progressToggle = false
@State var progressRingEndingValue: CGFloat = 0.75

var ringColor: Color = Color.green
var ringWidth: CGFloat = 20
var ringSize: CGFloat = 200

var body: some View {
    TabView{
        NavigationView{
            VStack{

                Spacer()

                ZStack{
                    Circle()
                        .trim(from: 0, to: progressToggle ? progressRingEndingValue : 0)
                        .stroke(ringColor, style: StrokeStyle(lineWidth: ringWidth, lineCap: .round, lineJoin: .round))
                        .background(Circle().stroke(ringColor, lineWidth: ringWidth).opacity(0.2))
                        .frame(width: ringSize, height: ringSize)
                        .rotationEffect(.degrees(-90.0))
                        .animation(.easeInOut(duration: 1))
                        .onAppear() {
                            self.progressToggle.toggle()
                        }

                    Text("\(Int(progressRingEndingValue * 100)) %")
                        .font(.largeTitle)
                        .fontWeight(.bold)
                }

                Spacer()

                Button(action: {
                    self.progressRingEndingValue = CGFloat.random(in: 0...1)
                }) { Text("Randomize")
                        .font(.largeTitle)
                        .foregroundColor(ringColor)
                }

                Spacer()

            }
            .navigationBarTitle("ProgressRing", displayMode: .inline)
            .navigationBarItems(leading:
                Button(action: {
                    print("Refresh Button Tapped")
                    }) {
                    Image(systemName: "arrow.clockwise")
                        .foregroundColor(Color.green)
                    }, trailing:
                Button(action: {
                    print("Share Button Tapped")
                    }) {
                    Image(systemName: "square.and.arrow.up")
                        .foregroundColor(Color.green)
                }
            )
        }
    }
}
}

#if DEBUG
struct ProgressRing_ContentView_Previews: PreviewProvider {
static var previews: some View {
    Group {

        ProgressRing_ContentView()
            .environment(\.colorScheme, .light)
            .previewDisplayName("Light Mode")

}
}
#endif

Above is the exact code that I'm currently working with. The actual animation of the ring sliding seems to be working how I expected it to, I'm just not sure why the entire ring itself is moving when embedded in a NavigationView.

1

1 Answers

20
votes

You need to use explicit animations, instead of implicit. With implicit animations, any animatable parameter that changes, the framework will animate. Whenever possible, you should use explicit animations. Below is the updated code. Notice I remove the .animation() call and added two withAnimation() closures.

If you would like to expand your knowledge on implicit vs. explicit animations, check this link: https://swiftui-lab.com/swiftui-animations-part1/

struct ContentView: View {

    @State var progressToggle = false
    @State var progressRingEndingValue: CGFloat = 0.75

    var ringColor: Color = Color.green
    var ringWidth: CGFloat = 20
    var ringSize: CGFloat = 200

    var body: some View {
        TabView{
            NavigationView{
                VStack{

                    Spacer()

                    ZStack{
                        Circle()
                            .trim(from: 0, to: progressToggle ? progressRingEndingValue : 0)
                            .stroke(ringColor, style: StrokeStyle(lineWidth: ringWidth, lineCap: .round, lineJoin: .round))
                            .background(Circle().stroke(ringColor, lineWidth: ringWidth).opacity(0.2))
                            .frame(width: ringSize, height: ringSize)
                            .rotationEffect(.degrees(-90.0))
                            .onAppear() {
                                withAnimation(.easeInOut(duration: 1)) {
                                    self.progressToggle.toggle()
                                }
                        }

                        Text("\(Int(progressRingEndingValue * 100)) %")
                            .font(.largeTitle)
                            .fontWeight(.bold)
                    }

                    Spacer()

                    Button(action: {
                        withAnimation(.easeInOut(duration: 1)) {
                            self.progressRingEndingValue = CGFloat.random(in: 0...1)
                        }
                    }) { Text("Randomize")
                        .font(.largeTitle)
                        .foregroundColor(ringColor)
                    }

                    Spacer()

                }
                .navigationBarTitle("ProgressRing", displayMode: .inline)
                .navigationBarItems(leading:
                    Button(action: {
                        print("Refresh Button Tapped")
                    }) {
                        Image(systemName: "arrow.clockwise")
                            .foregroundColor(Color.green)
                    }, trailing:
                    Button(action: {
                        print("Share Button Tapped")
                    }) {
                        Image(systemName: "square.and.arrow.up")
                            .foregroundColor(Color.green)
                    }
                )
            }
        }
    }
}