Reputation: 1218
I wrote the following to attempt to display a "arrow.down.circle.fill" and then when tapped display a progress view until tasks are done on the main tread. I then want to display the down arrow image again. I noticed it works fine when using a background thread, but then my UI in my main view doesn't update because published changes on background threads aren't allowed.
struct TestAnimation: View {
@State var isLoading: Bool = false
var body: some View {
if !isLoading {
Image(systemName: "arrow.down.circle.fill")
.font(.system(size: 35))
.padding()
.foregroundColor(.blue)
.imageScale(.large)
.onTapGesture {
DoSomthingMainTread()
}
}
else {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .blue))
.padding(30)
.scaleEffect(3)
}
}
func DoSomthingMainTread(){
DispatchQueue.global(qos: .background).async {
isLoading = true
DispatchQueue.main.async {
sleep(3) //call function here
}
isLoading = false
}
}
}
Thanks for any help!!
Upvotes: 0
Views: 479
Reputation: 285079
As mentioned by others never sleep
on the main thread
unless… you use Swift Concurrency, a Task
sleeps without blocking the thread
Task { @MainActor in
isLoading = true
try? await Task.sleep(nanoseconds: 3_000_000)
isLoading = false
}
Or if you want to do something in the background and update the loading state on the main thread create an extra function dispatched to the @MainActor
@MainActor func updateLoadingState(_ flag : Bool) {
isLoading = flag
}
and use a detached Task for the background work
Task.detached(priority: .background) {
await updateLoadingState(true)
doSomethingInBackground()
await updateLoadingState(false)
}
Upvotes: 1
Reputation: 156
Well, this problem is often encountered in my daily development and I prefer to use .redacted
first, but here I will provide a method base on your method.
First of all, in the view body it's better to use ZStack
, so the view will be changed into following code:
var body: some View {
ZStack {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .blue))
.padding(30)
.scaleEffect(3)
.opacity(isLoading ? 1: 0)
Image(systemName: "arrow.down.circle.fill")
.font(.system(size: 35))
.padding()
.foregroundColor(.blue)
.imageScale(.large)
.onTapGesture {
DoSomthingMainTread()
}
.opacity(isLoading ? 0: 1)
}
}
Notice here I use .opacity
to decide whether to show the Image
or not. And in the DoSomthingMainTread()
method, change the code to:
func DoSomthingMainTread(){
isLoading = true
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
isLoading = false
}
}
That's my answer, hope it's useful for you.
Upvotes: 1
Reputation: 385600
You are doing things on the wrong queues.
DispatchQueue.global(qos: .background).async {
isLoading = true // NO!!!! This needs to be on the main queue.
DispatchQueue.main.async {
sleep(3) // NO!!! This should be off the main queue.
}
isLoading = false // NO!!! This needs to be on the main queue.
}
You must update isLoading
on the main queue, because it is a property of a View
. And you should not perform slow, blocking work (like sleep(3)
) on the main queue, because it prevents the user interface from responding to user actions.
Your DoSomthingMainTread
method should look like this:
func DoSomthingMainTread(){
// Already on main queue because onTapGesture's body runs on main queue.
isLoading = true
DispatchQueue.global(qos: .background).async {
// On background queue, so OK to perform slow, blocking work.
sleep(3) //call function here
DispatchQueue.main.async {
// Back on main queue, so OK to update isLoading.
isLoading = false
}
}
}
Upvotes: 1