I'm making a loading screen, but I can't figure out how to make the loading indicator animate smoothly while work is being performed. I've tried a variety of tactics, including creating confining the animation to a .userInitiated
Task, or downgrading the loading Task to .background
, and using TaskGroups. All of these resulted in hangs, freezes, or incredibly long load times.
I've noticed that standard ProgressViews work fine when under load, but the documentation doesn't indicate why this is the case. Customized ProgressViews don't share this trait (via .progressViewStyle()
) also choke up. Finding out why might solve half the problem.
Note: I want to avoid async complications that come with using nonisolated
functions. I've used them elsewhere, but this isn't the place for them.
It’s hard to offer a specific answer without more details but, in general, the canonical way to do this is to ensure that only work that must be isolated to the main actor runs on the main actor.
I want to avoid async complications that come with using nonisolated functions.
Understood. The main actor is the right choice for all your UI work. You can also use it for ‘command and control’ work, like getting the results of a background operation and use it to kick off the next background operation. However, you’ve gotta avoid doing significant work on the main actor, and that includes both CPU bound work and synchronous I/O.
Consider this rudimentary example:
1 struct ContentView: View {
2 @State var lastPhotoID: String = "none"
3 var body: some View {
4 VStack {
5 Text(lastPhotoID)
6 }
7 .padding()
8 .task {
9 do {
10 let url = URL(string: "https://photos.example.com/photo/12345")!
11 let request = URLRequest(url: url)
12 let (data, response) = try await URLSession.shared.data(for: request)
13 … check response …
14 let photo = try decompressPhoto(data)
15 try savePhoto(photo)
16 self.lastPhotoID = photo.id
17 } catch {
18 // …
19 }
20 }
21 }
22 }
There are three points of concern here:
-
Line 12, which uses
URLSession
to fetch the photo data -
Line 14, which uses the CPU to decompress it
-
Line 15, which saves it to disk
Line 12 is fine because the URLSession
is async. However, lines 14 and 15 are problematic. This code is running on the main action — you can tell that because it can reference lastPhotoID
— so the CPU work in line 14 and the synchronous I/O work in line 15 could potentially affect responsiveness.
My preferred way to resolve issues like this is to move that work to an asynchronous function. So, something like this:
func fetchPhoto(url: URL) async throws -> String {
let request = URLRequest(url: url)
let (data, response) = try await URLSession.shared.data(for: request)
… check response …
let photo = try decompressPhoto(data)
try savePhoto(photo)
return photo.id
}
The remaining code on the main actor is all ‘fast’, and so won’t affect responsiveness.
This asynchronous function could be an nonisolated
method on your view, but IMO it’s better to come up with an abstraction that gets this work out of your view entirely. The obvious place for such work is in an actor, but that’s not the only game in town. There are lots of design options in this space.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"