1

Using Swift5.5, iOS15.0.1,

I had to realise that my circular ProgressBar does no longer animate after updating to iOS15.

  1. Below is my code - can anybody tell me what to do in order to make the circular ProgressBar-View animate again ?

  2. Can anybody tell me how to circumvent in this example the deprecation-warning animation' was deprecated in iOS 15.0: Use withAnimation or animation(_:value:) instead. ?

import SwiftUI

struct ContentView: View {
    
    @State var progressValue: Float = 0.28
    
    var body: some View {
        ProgressBar(progress: $progressValue)
    }
}

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

struct ProgressBar: View {
    @Binding var progress: Float
    
    var body: some View {
        ZStack {
            Circle()
                .stroke(lineWidth: 20.0)
                .opacity(0.3)
                .foregroundColor(Color.red)
            
            Circle()
                .trim(from: 0.0, to: CGFloat(min(self.progress, 1.0)))
                .stroke(style: StrokeStyle(lineWidth: 20.0, lineCap: .round, lineJoin: .round))
                .foregroundColor(Color.red)
                .rotationEffect(Angle(degrees: 270.0))
                .animation(.linear)
            Text(String(format: "%.0f %%", min(self.progress, 1.0)*100.0))
                .font(.largeTitle)
                .bold()
        }
    }
}

My original version looked like this:

Circle()
.trim(from: 0, to: showFreespaceRing ? CGFloat(Double(freeDiskspace) / Double(totalDiskspace)) : 0)
.stroke(Color.green.opacity(0.7), style: StrokeStyle(lineWidth: 10, lineCap: .round))
.frame(width: circleDiam, height: circleDiam)
.animation(.easeIn(duration: 1))
.onAppear() {
    showFreespaceRing = true
}
iKK
  • 6,394
  • 10
  • 58
  • 131

2 Answers2

1

The value parameter takes an Equatable that represents the value that is animating. In this case : progress.

I also moved .animation outside of the ZStack -- otherwise, I was seeing a funny jitter on the animation.

struct ContentView: View {
    
    @State var progressValue: Float = 0.28
    
    let timer = Timer.publish(every: 1.0, on: .main, in: .default).autoconnect()
    
    var body: some View {
        ProgressBar(progress: $progressValue)
            .onReceive(timer) { _ in
                progressValue += 0.1
            }
    }
}

struct ProgressBar: View {
    @Binding var progress: Float
    
    var body: some View {
        ZStack {
            Circle()
                .stroke(lineWidth: 20.0)
                .opacity(0.3)
                .foregroundColor(Color.red)
            
            Circle()
                .trim(from: 0.0, to: CGFloat(min(self.progress, 1.0)))
                .stroke(style: StrokeStyle(lineWidth: 20.0, lineCap: .round, lineJoin: .round))
                .foregroundColor(Color.red)
                .rotationEffect(Angle(degrees: 270.0))
            Text(String(format: "%.0f %%", min(self.progress, 1.0)*100.0))
                .font(.largeTitle)
                .bold()
        }
        .animation(.linear, value: progress)
    }
}
jnpdx
  • 45,847
  • 6
  • 64
  • 94
  • Thank you for your solution. I find the timer odd, since this does not give the behaviour of the circle-animation that I would expect. I need a very smooth animation from zero to the progress upon startup. And not from progress until 100% every second as in your example. Do you agree that the original example is not really easily replaced by the iOS15 change ? – iKK Oct 12 '21 at 15:54
  • @iKK The original example wouldn't have done that either, in any version of iOS. Your original code had a static value of 0.28. How often would you like to update it? Is it not supposed to go in chunks -- just smoothly from 0.28 to 100? Over what period of time? Seems like there might be a lot of detail missing in the question. – jnpdx Oct 12 '21 at 15:56
  • You are right, the original question did not show enough. See on previous iOS version I was simply able to add `.animation(.easeIn(duration: 1))` modifier on Circle(). And the OS did all for me (i.e. smooth easeIn over 1 sec to whatever value it needed to go). But now unter iOS15 not anymore. I somehow need to figure out which value was animated and need to build it myself. And I don't like the idea of a timer necessity. Isn't there another way to make the OS do the animation as before ? – iKK Oct 12 '21 at 16:30
  • I still don't know what "as before" means -- your original code had one static value. Under what circumstances would you like it to animate? I used a `Timer` just to demonstrate the concept. Usually with a progress indicator, you're basing it on some external factor. I'm not sure how it ever would've animated. You can still use `.easeIn(duration: 1)` -- I used `.linear` because that's what you used in your original example. – jnpdx Oct 12 '21 at 16:34
  • See my original code in Example inserted at the end of original post. As you can see I only set a property `showFreespaceRing` to true inside the `onAppear()` and the whole animation took place. The `.trim` was somehow responsible for the animation. But no other progress-property needed. Now under iOS15, I guess, I need to place an extra `progress` property inside trim to make the animation work again. – iKK Oct 13 '21 at 07:42
  • I addressed that flickering in my answer by putting the animation modifier outside the Zstack – jnpdx Oct 13 '21 at 14:44
0

I finally found out was went wrong:

On my original version, I had

Circle()
    .trim(from: 0, to: showFreespaceRing ? CGFloat(Double(freeDiskspace) / Double(totalDiskspace)) : 0)
    .stroke(Color.green.opacity(0.7), style: StrokeStyle(lineWidth: 10, lineCap: .round))
    .frame(width: circleDiam, height: circleDiam)
    .animation(.easeIn(duration: 1))
    .onAppear() {
        showFreespaceRing = true
    }

And now I have:

Circle()
    .trim(from: 0, to: showFreespaceRing ? 0 : CGFloat(Double(freeDiskspace) / Double(totalDiskspace)))
    .stroke(Color.green.opacity(0.7), style: StrokeStyle(lineWidth: 10, lineCap: .round))
    .frame(width: circleDiam, height: circleDiam)
    .animation(.easeIn(duration: 1), value: showFreespaceRing)
    .onAppear() {
        showFreespaceRing.toggle()
    }

With this, the animation works again (somewhat - see video below, there is still one issue)....

The trick was to use .toggle() inside the onAppear method.

What definitively does not work is to have showFreespaceRing = true inside the onAppear() method (but rather showFreespaceRing.toggle() instead !!!!!!!

And, of course, fulfilling iOS15's new value inside animation:

.animation(.easeIn(duration: 1), value: showFreespaceRing)

However, there is one annoyance, still, with the current solution:

THE ANIMATION IS NOT SMOOTH !!

See this video:

If you look carefully you can see that the animation is not smooth at all but rather flickering badly. (i.e. the ring-animation looks catastrophic, still). How can I get a smooth animation ??

enter image description here

=================================================

After 1 day of investigation, here is what I've found out:

THE ROOT CAUSE OF THE JITTER IN THE ANIMATION SEEMS TO BE THE .sheet !!!!

Here is a complete example that jitters !

import SwiftUI

enum THSheetSelection: Hashable, Identifiable {
        
    case trialSheet
    
    var id: THSheetSelection { self }
}


struct ContentView: View {
    
    @State private var sheetState: THSheetSelection?
    
    var body: some View {
        Text("Hello")
            .onAppear {
                DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(2000)) {
                    sheetState = .trialSheet
                }
            }
            .sheet(item: $sheetState) { state in
                switch state {
                case .trialSheet:
                    SessionDiskspaceStatisticsView()
                }
            }
    }
}

struct SessionDiskspaceStatisticsView: View {
    
    @State private var neededDiskSpace: Int64 = 0
    @State private var freeDiskspace: Int64 = 0
    @State private var totalDiskspace: Int64 = 0
    
    @State private var showFreespaceRing = false
    
    let deviceIdiom = UIScreen.main.traitCollection.userInterfaceIdiom
    
    var body: some View {
        ZStack {
            VStack(alignment: .center) {
                Spacer().frame(height: 30)
                ZStack {
                    HStack {
                        Spacer().frame(width: 50)
                        ZStack{
                            Circle()
                                .trim(from: 0, to: 1)
                                .stroke(Color.gray.opacity(0.2), lineWidth: 10)
                                .frame(width: 137.5, height: 137.5)
                                
                                if neededDiskSpace < freeDiskspace {
                                    Circle()
                                        .trim(from: 0, to: showFreespaceRing ? 0 : CGFloat(Double(freeDiskspace) / Double(totalDiskspace)))
                                        .stroke(Color.green.opacity(0.7), style: StrokeStyle(lineWidth: 10, lineCap: .round))
                                        .frame(width: 137.5, height: 137.5)
                                        .animation(.easeIn(duration: 1), value: showFreespaceRing)
                                        .onAppear() {
                                            showFreespaceRing.toggle()
                                        }
                                    .rotationEffect(.init(degrees: 90))
                                } else {
                                    Text(LocalizedStringKey("NotEnoughSpaceKey"))
                                        .font(.system(size: 16))
                                        .fontWeight(.regular)
                                        .foregroundColor(Color.red)
                                        .frame(width: 137.5 - 10)
                                        .multilineTextAlignment(.center)
                                        .rotationEffect(.init(degrees: 90))
                                }
                        }
                        .rotationEffect(.init(degrees: -90))
                        Spacer()
                    }
                }
                Spacer().frame(height: 30)
            }
        }
        .onAppear {
            
            neededDiskSpace = 20000
            freeDiskspace = 130000
            totalDiskspace = 150000
        }
    }
}

Any idea on how to animate the circular-progressBar inside a .sheet without Jitter ??????

iKK
  • 6,394
  • 10
  • 58
  • 131
  • Did you try putting the `.animation` modifier on the `ZStack` as I referenced in both my answer and my comment? That solved the flickering for me. – jnpdx Oct 14 '21 at 07:34
  • yes, I did - an no, it did not help ! Did you get my small example get to run smooth with your "trick" ? (i.e. I do not use progress-steps of 0.01 but rather set the `showFreespaceRing`-Boolean to true when animation should start: `.animation(.easeIn(duration: 1), value: showFreespaceRing)` – iKK Oct 14 '21 at 09:22
  • See what I've found out in my answer with video ! The root-cause of the jitter seems to be the fact that everything is inside a `.sheet`. I added a minimal example that shows the opening of a .sheet after 2 seconds. The animation starts but it jitters clearly. And in my original example, I also have a .sheet. !! What is there to do ? – iKK Oct 14 '21 at 10:34