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.
com.google.android.material.navigation.NavigationView. Represents a standard navigation menu for application. The menu contents can be populated by a menu resource file. NavigationView is typically placed inside a DrawerLayout .
SwiftUI has built-in support for animations with its animation() modifier. To use this modifier, place it after any other modifiers for your views, tell it what kind of animation you want, and also make sure you attach it to a particular value so the animation triggers only when that specific value changes.
SwiftUI uses Core Animation for rendering by default, and its performance is great for most animations. But if you find yourself creating a very complex animation that seems to suffer from lower framerates, you may want to utilize the power of Metal, which is Apple's framework used for working directly with the GPU.
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)
}
)
}
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With