micah
micah

Reputation: 1218

SwiftUI toggle State bool onTapGesture using main tread

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

Answers (3)

vadian
vadian

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

Huang Runhua
Huang Runhua

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

rob mayoff
rob mayoff

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

Related Questions