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
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 onEventStates
, say inonChange(of:)
modifier.You can try to use .id(UUID()) modifier. With views redrawing it will recreate a new view and the animation will be visible