skip to Main Content

I am using custom activity indicator and I see it animating only when I call it at certain places in the view. Adding the code first for better understanding.

Activity Indicator:

import SwiftUI

struct ActivityIndicator: View {
    @Binding var shouldAnimate: Bool
    private let count: Int = 5
    private let element = AnyView(Circle().frame(width: 15, height: 15))

    public var body: some View {
        GeometryReader { geometry in
            ForEach(0..<count, id: .self) { index in
                item(forIndex: index, in: geometry.size)
                    .rotationEffect(shouldAnimate ? .degrees(360) : .degrees(0))
                    .animation(
                        Animation
                            .timingCurve(0.5, 0.15 + Double(index) / 5, 0.25, 1, duration: 2.0)
                            .repeatCount(shouldAnimate ? .max : 1, autoreverses: false)
                    )
                    .frame(width: geometry.size.width, height: geometry.size.height)
            }
        }
        .aspectRatio(contentMode: .fit)
    }
    
    private func item(forIndex index: Int, in geometrySize: CGSize) -> some View {
        element
            .scaleEffect(shouldAnimate ? (CGFloat(index + 1) / CGFloat(count)) : 1 )
            .offset(y: geometrySize.width/10 - geometrySize.height/2)
    }
}

CustomView code

// Activity indicator works in this code.
import SwiftUI

struct CustomView: View {
    @ObservedObject var viewModel: EventsListViewModel
    @State var shouldAnimate: Bool = false
    
    var body: some View {
        VStack {
            // Activity indicator works here, but does not disappear when should animate is set to false in ".dataReady"
            ActivityIndicator(shouldAnimate: $shouldAnimate)
            
            switch viewModel.state {
            case .idle:
                Color.clear.onAppear(perform: { viewModel.getData() })
            case .loading:
                EmptyView()
            case .dataReady(let eventsList):
                EventsListView(eventsList: eventsList, viewModel: viewModel).onAppear() {
                    self.shouldAnimate = false
                }
        }.navigationBarTitle("Back")
        .onAppear() {
            self.shouldAnimate = true
            viewModel.state = .idle
        }
    }
}

// Activity indicator does not animate in this code.
import SwiftUI

struct CustomView: View {
    @ObservedObject var viewModel: EventsListViewModel
    @State var shouldAnimate: Bool = false
    
    var body: some View {
        VStack {
            switch viewModel.state {
            case .idle:
                Color.clear.onAppear(perform: { viewModel.getData() })

            case .loading:
                // Activity indicator does not work here i.e. doesn't animate. I just see one circle on the view. 
                // However once ".dataReady" is executed, the activity indicator does disappear.
                ActivityIndicator(shouldAnimate: $shouldAnimate)

            case .dataReady(let eventsList):
                EventsListView(eventsList: eventsList, viewModel: viewModel).onAppear() {
                    self.shouldAnimate = false
                }
        }.navigationBarTitle("Back")
        .onAppear() {
            self.shouldAnimate = true
            viewModel.state = .idle
        }
    }
}

View model code:

import Foundation
import Combine

class EventsListViewModel: ObservableObject {
    enum EventStates {
        case idle
        case loading
        case dataReady([EventsModel])
    }
    @Published var state = EventStates.idle
    
    init() {
        NotificationCenter.default.addObserver(forName: Notification.Name("eventsListResponse"), object: nil, queue: nil, using: self.processEventsList(_:))
    }
    
    @objc func processEventsList(_ notification: Notification) {
        // Collect data from notification object
        self.state = .dataReady(eventsList)
     }
    
    func getData() {
        self.state = .loading
        // do something
    }
}

When my CustomView appears, view model’s state is set to idle which makes a request to get the data to display on the view. While the request is in process, state is set to ".loading", hence I was calling ActivityIndicator() in .loading, but when I call the activity indicator from "switch" within the view, then I don’t see the indicator animating(I just see one static circle for activity indicator on the view). But if I place the call outside of the view, then the activity indicator works fine, however it doesn’t disappear when I set self.shouldAnimate to false within switch.

Before I add custom activity indicator, I was using ProgressView() under .loading and that was working fine.

Any idea why activity indicator is not working within switch? Or if it cannot work within switch, then I can keep it outside the switch, but why does it not disappear when self.shouldAnimate is set to false?

Thanks!

Edit: While doing some research on onChange after Asperi’s post, I came across onReceive(), tried it and along with that I am seeing the activity indicator appear on the view, but it doesn’t disappear when shouldAnimate is set to false.

import SwiftUI

struct CustomView: View {
    @ObservedObject var viewModel: EventsListViewModel
    @State var shouldAnimate: Bool = false
    
    var body: some View {
        VStack {
            // Activity indicator works here, but does not disappear when should animate is set to false in ".dataReady"
            ActivityIndicator(shouldAnimate: $shouldAnimate)
            
            switch viewModel.state {
            case .idle:
                Color.clear.onAppear(perform: { viewModel.getData() })
            case .loading:
                EmptyView()
            case .dataReady(let eventsList):
                EventsListView(eventsList: eventsList, viewModel: viewModel)
        }.navigationBarTitle("Back")
        .onAppear() {
            viewModel.state = .idle
        }
        .onReceive(viewModel.$state) { (value) in
            switch value {
                case .idle:
                    self.shouldAnimate = false
                case .loading:
                    self.shouldAnimate = true
                case .dataReady(_):
                    self.shouldAnimate = false
            }
        }
    }
}

2

Answers


  1. Animation happens "on-change"; in the first case indicator view is present and observes changes, in the second case it just appears with already set state, so there are not internal changes, so no animation happens.

    Conclusion: use first variant and just toggle shouldAnimate depending on EventStates, say in onChange(of:) modifier.

    Login or Signup to reply.
  2. You can try to use .id(UUID()) modifier. With views redrawing it will recreate a new view and the animation will be visible

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search