Thanks for the prompt response!
I managed to solve the issue after doing all the things I've avoided doing out of laziness and drilling deep into my code, and then realizing that my implementation of the animation was either the root cause or a major contributor
Uninteresting post-mortem
- I excised all state for the loading screen into its own view model, granting me more control over its concurrency.
- I then designated the animation function
@MainActor
and the workload nonisolated
(I'm a terrible person) - These failed to solve the issue, so after jiggling a lot of knobs I drilled deep into the workload code
- This yielded a function that ran on @MainActor deep into the code, so I fixed that and the issue still wasn't fixed.
- More knobs were jiggled, no help
- Realized the animation itself was poorly implemented since I wrote that when I was very new to coding
Long story short There's a beachball-like image that rotates 360 degrees during loading, but back then I didn't know about .repeatForever()
, so instead I jury rigged an abomination:
Task {
while loading {
try await Task.sleep(for: .seconds(0.5))
withAnimation(.linear(duration: 0.5)) {
beachballRotation += 90
}
}
}
The issues went away after replacing all that with:
withAnimation(.linear(duration: 2).repeatForever(autoreverses: false)) {
beachballRotation = 360
}
The animation still jumps at the 360 degree mark, when the animation repeats itself. Other than that, everything works smoothly, so I'm sure I'll eventually figure something out.
I bet moving everything to a view model and setting the workload to nonisolated also helped (again, I'm a terrible person and I'm sorry). My guess is timing imperfections + frequent new calls during heavy load processing lead to the sputtering and freezing.
But all of this has reminded me of a question I've always had:
In the process of moving to nonisolated, I've had to disfigure my code by inserting DispatchQueue.main.async
closures. This is my go-to solution whenever a purple or yellow warning pops up telling me some async code should be on the main thread. But, every time I do this, there's a little voice at the back of my head reminding me of a warning a younger me received about never crossing the streams. i.e. One should never mix the antiquated DispatchQueue.main.async
/GCD with the modern async-await.
I'm a SwiftUI native, and all my concurrency is technically async-await. At last count I have 150 instances where I've crossed the streams. Many of these were done out of desperation because my app makes heavy use of SpriteKit, whose SKScene demands all calls to be on the main queue. Everything seems to work fine for now, but this is still something I'd like to fix before it blows up in my face or Apple finally makes it a bona-fide error.
tl;dr - How does one avoid using DispatchQueue.main.async
to encapsulate async-await code that should be on main?
@DTS Engineer if you feel this deserves its own thread, please let me know and I'll make one.