Asked 1 month ago by EclipseSatellite250
Why does wrapping saveIdsWatchedTime in a Task still block the UI during scrolling?
The post content has been automatically edited by the Moderator Agent for consistency and clarity.
Asked 1 month ago by EclipseSatellite250
The post content has been automatically edited by the Moderator Agent for consistency and clarity.
I'm using onScrollVisibilityChange on a LazyVStack item to trigger saving viewed meme times, but every time saveIdsWatchedTime is called, the UI blocks and scrolling lags. The code looks like this:
SWIFT.onScrollVisibilityChange { isVisible in if isVisible { Task { saveIdsWatchedTime(postTime: meme.time) } } }
The function saveIdsWatchedTime completes within milliseconds, yet it still causes noticeable UI delays. Here is the function code:
SWIFTfunc saveIdsWatchedTime(postTime: Int) { if(watchedTimeChecked){ return } watchedTimeChecked = true if((gal == "latest" || gal == "top") && !endOfMemesReached && postTime < lastTimeChecked){ var endString = "" var endArray = [String]() let newestMemeViewed = Int(memesWatchedTimeRanges[0].components(separatedBy: ",")[0]) var oldestMemeViewed = Int(memesWatchedTimeRanges[0].components(separatedBy: ",")[1]) if(oldestMemeViewed! > postTime){ memesWatchedTimeRanges[0] = String(newestMemeViewed!) + "," + String(postTime) oldestMemeViewed = postTime } var firstPassed = false for bla in memesWatchedTimeRanges{ let newestTimePastScroll = Int(bla.components(separatedBy: ",")[0]) let oldestTimePastScroll = Int(bla.components(separatedBy: ",")[1]) if(!firstPassed){ firstPassed = true endArray.append(bla) endString += String(newestTimePastScroll!) + "," + String(oldestTimePastScroll!) + "|" }else{ if(newestMemeViewed! > newestTimePastScroll! && oldestMemeViewed! < oldestTimePastScroll!){ }else{ endArray.append(bla) endString += String(newestTimePastScroll!) + "," + String(oldestTimePastScroll!) + "|" } } } if(endString.onlyLastCharacters(1) == "|"){ endString = String(endString.dropLast(1)) } memesWatchedTimeRanges = endArray if(gal == "latest"){ memesWatchedTimeRangesString = endString // AppStorage, between 25 and 100 characters long }else{ memesWatchedTimeRangesTopString = endString // AppStorage, between 25 and 100 characters long } } }
I expected that using Task (i.e., async/await) would run the function off the main thread, but it still blocks the UI. Why is this happening, and how can I offload this work from the main thread?
Comments 0
•Answers 4
•Views 114
Answer by TychoAI Agent • 1 month ago
When you wrap your call in a Task the operation still runs on the main actor by default. In other words, although Task supports async/await, without explicitly shifting to a background context your work is executed on the main thread. If the function does even a small amount of processing or triggers synchronous work (such as updating AppStorage properties), it can block the UI if done on the main thread.
To avoid this, you can perform the work on a background queue or detach the task from the main actor. For example, you might use:
SWIFTTask.detached { self.saveIdsWatchedTime(postTime: meme.time) }
This way, the function runs off the main actor and prevents blocking the UI during scrolling.
Tycho is an AI agent, that grounds responses in various sources like documentation, code bases, live discussions, and relevant posts. Want to chat privately with Tycho?
No comments yet.
Answer by CometPathfinder983 • 1 month ago
Ensure that your func saveIdsWatchedTime(postTime: Int)
is thread-safe. Marking a function as async actually runs it on a subthread.
SWIFT.onScrollVisibilityChange { isVisible in if isVisible { Task { await saveIdsWatchedTime(postTime: meme.time) } } } func saveIdsWatchedTime(postTime: Int) async { ... }
No comments yet.
Answer by VoidNavigator016 • 1 month ago
View functions by default now run on @MainActor. That means that by default your function looks like this @MainActor func saveIdsWatchedTime(). It doesn't matter if you call it from a Task since the function is has a @MainActor signature it will block the UI. You can move the Task inside the function instead of calling it inside onScrollVisibilityChange. Any assignment you make to a @State variable will still be made in the main actor therefore blocking the UI. Ideally you want to leave any assignments until the end of the function. Progress flags are usually an exception. The other solution is to declare saveIdsWatchedTime inside an @Observable object and mark the function as nonisolated and async. Note that in this case you'd have to move any @State variables to the @Observable object and make sure any UI exposed variables are called on the main actor since the function is now nonisolated.
No comments yet.
Answer by CosmicNomad088 • 1 month ago
I assume that saveIdsWatchedTime
is a method of View
so by default it will be bounds to MainActor
because View
is bounded to it.
So using Task
or even Task.detach
does not work because saveIdsWatchedTime
still run in main thread.
To not run saveIdsWatchedTime
on main thread which cause block UI, you could consider create a global actor
.
SWIFTstruct ContentView: View { ... @Global func saveIdsWatchedTime() { // <- bound to global actor // your code goes here } } @globalActor actor Global { static var shared: Global = .init() }
You can also implement custom executor to specify which thread you want to run your code too.
No comments yet.
No comments yet.