1

I made a reusable circular progress bar. I applied repeatForever animation to keep it rotating but it only works when used directly with a @State or @Published variable and doesn't work when it is embedded in another view.

Reusable RingView. Which is a circular progress bar

struct RingView: View {
    private let percent: CGFloat = 80   // Keeping it fixed 80 for test purpose
    var color = Color.primaryLightColor // Some random color
    @Binding var show: Bool

    var body: some View {
        ZStack {
            GeometryReader { bounds in

                Circle()
                    .trim(from: self.show ? self.progress : 1, to: 1)
                    .stroke(
                        LinearGradient(gradient: Gradient(colors: [self.color]), startPoint: .topTrailing, endPoint: .bottomLeading),
                        style: StrokeStyle(lineWidth: self.lineWidth(width: bounds.size.width), lineCap: .round, lineJoin: .round, miterLimit: .infinity, dash: [20, 0], dashPhase: 0)
                )
                    .rotationEffect(Angle(degrees: 90))
                    .animation(.none)
                    .frame(width: bounds.size.width, height: bounds.size.height)
            }
        }
        .rotationEffect(.degrees(show ? 360.0 : 0.0))
        .animation(show ? Animation.linear(duration: 1.0).repeatForever(autoreverses: false) : .none)
    }

    func multiplier(width: CGFloat) -> CGFloat {
        width / 44
    }

    func lineWidth(width: CGFloat) -> CGFloat {
        5 * self.multiplier(width: width)
    }

    var progress: CGFloat {
        1 - (percent / 100)
    }
}

Progress Button. It uses the RingView above. A general use case is to show animation when doing a long background task.

The RingView animation doesn't work in this ProgressButton

struct ProgressButton: View {
        var action: () -> Void
        var image: Image? = nil
        var text: String = ""
        var backgroundColor: Color = Color.blue
        var textColor: Color = .white
        @Binding var showProgress: Bool

        var body: some View {
            Button(action: action) {
                HStack {
                    if showProgress {
                        RingView(color: textColor, show: self.$showProgress)
                            .frame(width: 25, height: 25)
                        .transition(.scale)
                            .animation(.Spring())
                    } else {
                        image?
                            .renderingMode(.original)
                            .resizable()
                            .frame(width: 25, height: 25)
                    }
                    Text(text)
                        .font(.headline)
                        .foregroundColor(textColor)
                }
                .padding()
                .frame(minWidth: 0, maxWidth: .infinity, minHeight: 50, alignment: .center)
                .background(backgroundColor)
                .clipShape(RoundedRectangle(cornerRadius: 5, style: .continuous))
            }.buttonStyle(PlainButtonStyle())
        }
    }

When RingView animation works. When I use it directly as below.

struct LoginView: View {
    @State var showProgress = true
    var body: some View {
        VStack {

            // WORKING
            RingView(show: self.$showProgress)
                .frame(width: 25, height: 25)

            // NOT WORKING
            ProgressButton(action: {
                self.showProgress = true
            }, image: Image("password-lock"), text: "Login", backgroundColor: .blue, showProgress: self.showProgress)
        }
    }
}

I think I'm doing some mistake understanding how @Binding and @State are working.

2
  • i tried to reproduce your "working" case with loginview -> but it didnt work...maybe you can recheck your example or even better: make it reproducible AND compilable...thank you Commented May 28, 2020 at 18:19
  • @Chris I've got the answer but have edited my code and have removed custom strings and variables in this might help someone. Thanks for reaching out. Commented May 29, 2020 at 4:06

1 Answer 1

3

Animation is activate on state change. In provided code there is no change so no animation at all.

Below are main changes so I made it work. Tested with Xcode 11.4 / iOS 13.4 (with some replication of absent custom dependencies)

demo

  1. Made initial animation off
struct LoginView: View {
    @State var showProgress = false

// ... other code

    // no model provided so used same state for progress
    ProgressButton(action: {
        self.showProgress.toggle()     // << activate change !!!
    }, image: Image("password-lock"), text: L.Login.LoginSecurely, backgroundColor: self.isLoginButtonEnabled ? Color.primaryLightColor : Color.gray, showProgress: $showProgress) 

  1. Add internal ring activating state inside progress button; same state used for hide/unhide and activating does not work, again, because when ring appeared there is no change (it is already true) so ring is not activated

    @State private var actiavteRing = false // << here !! var body: some View { Button(action: action) { HStack {

             if showProgress {
                 RingView(color: textColor, show: self.$actiavteRing) // !!
                     .frame(width: 25, height: 25)
                     .transition(.opacity)
                     .animation(.spring())
                     .onAppear { self.actiavteRing = true } // << 
                     .onDisappear { self.actiavteRing = false } // <<
    
  2. in Ring fixed animation deactivation to avoid cumulative effect (.none does not work here), so

    .rotationEffect(.degrees(show ? 360.0 : 0.0)) .animation(show ? Animation.linear(duration: 1.0).repeatForever(autoreverses: false) : .default) // << here !!

Sign up to request clarification or add additional context in comments.

2 Comments

Thanks for your quick help. I am still confused with this part: Why didn't it worked when I used @Binding showProgress from ProgressButton with RingView's show variable. Does it not pass state changes from Login to ProgressButton and then to RingView ?
@harsh_v, it is passed, but at the moment when RingView appears it is initiated with already true value, so there is no trigger to activate animation.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.