How to do asynchronous action with SwiftUI button
Asked Answered
A

1

8

I want to click a button in SwiftUI that will trigger a JSON encoding action. This action is time consuming thus I need it to be async. I have already tried two solutions but they do not work. One major problem is how to create a async version of the json encoding?

Solution 1)

public func encodeJSON<T>(_ value: T, encoder: JSONEncoder, completionHandler: @escaping (Data?, Error?) -> Void) where T: Encodable {
        DispatchQueue.global().async {
            do {
                let data = try encoder.encode(value)
                DispatchQueue.main.async {
                    completionHandler(data, nil)
                    print("finish encode json")
                }
            } catch {
                DispatchQueue.main.async {
                    completionHandler(nil, error)
                    print("fail encode json")
                }
            }
        }
    }
    
    public func encodeJSON<T>(_ value: T, encoder: JSONEncoder) async throws -> Data where T: Encodable {
        
        try await withUnsafeThrowingContinuation { continuation in
            encodeJSON(value, encoder: encoder) { data, error in
                if let error = error {
                    continuation.resume(throwing: error)
                } else if let data = data {
                    continuation.resume(returning: data)
                } else {
                    fatalError()
                }
            }
        }
    }

And I call the function in SwiftUI body:

Button {
                
                let localDate = dailyUploadRecord.date!
                let impactMed = UIImpactFeedbackGenerator(style: .medium)
                impactMed.impactOccurred()
 
                
               
                guard let file = UploadFileManager.shared.fetchedResults else { return }
                
                let encoder = JSONEncoder()
                encoder.dateEncodingStrategy = .millisecondsSince1970
                
                Task {
                    isEncoding = true
                    let result = try await uploadManager.encodeJSON(file, encoder: encoder)
                    print(result)
                    isEncoding = false
                }

            } label: {
                Text(“TEST")
                    .overlay {
                        if isEncoding {
                            ProgressView()
                        }
                    }
            }
            .disabled(isEncoding)
            .buttonStyle(.bordered)

However, it gave me the runtime error: Thread 6: EXC_BREAKPOINT (code=1, subcode=0x1b338b088)

Then, I tried the second solution:

public func encodeJSON<T>(_ value: T, encoder: JSONEncoder) async throws -> Data where T: Encodable {
        return try encoder.encode(value)
    }
Button {
                
            let localDate = dailyUploadRecord.date!
            let impactMed = UIImpactFeedbackGenerator(style: .medium)
            impactMed.impactOccurred()
                
            guard let file = UploadFileManager.shared.fetchedResults else { return }
                
            let encoder = JSONEncoder()
            encoder.dateEncodingStrategy = .millisecondsSince1970
                
            Task {
                isEncoding = true
                let result = try await encodeJSON(file, encoder: encoder)
                print(result)
                isEncoding = false
            }

        } label: {
            Text(“TEST")
                .overlay {
                    if isEncoding {
                        ProgressView()
                    }
                }
        }
        .disabled(isEncoding)
        .buttonStyle(.bordered)

However, the ui is freezed and when the encodeJSON is finished, it return to normal and I can interact with.

My question is: How to create an async version of JSONEncoder().encode(value: Data) and call it in the Button of SwiftUI, without blocking the main thread (make the UI freezed)? Any suggestion is welcomed!

I tried two solutions. One is create a async version from the DispatchQueue.global().async {} and convert it. The other is directly wrap the JSONEncoder().encode(value: Data) in a async function. However, the two solutions did not work.

I expect to click the Button and the related encoding function could execuate asynchronously.

Averill answered 2/4, 2022 at 17:4 Comment(4)
Use a View Model, anObervableObject and do the heavy work there. And rather than DispatchQueue and completion handlers use async/await.Poky
Thanks for your comments. Could you please provide some more details? How to let the view model do the heavy work without block the UIAverill
You need lean more about @MainActor and Task { @MainActor in …. 1. You need mark async method with @MainActor, for execute task in main actor with high priority, not in background priority. 2. Create a local sync func that do async work like this: func encodeJSON() { Task { @MainActor in await viewModel.encodeJSON(_ value:)Impanel
Does this answer your question? Trouble running async functions in background threads (concurrency)Braiding
B
7

The second way is correct. The problem is likely that the encoder is throwing an exception which happens when something in the data cannot be encoded. This means the isEncoding = false line is not reached and the UI is stuck in the encoding state. Fix it like this:

.task(id: isEncoding) {
    if isEncoding == false {
       return
    }
       do {
           let result = try await encodeJSON(file, encoder: encoder)
            print(result)
        }
        catch {
           print(error.localizedDescription)
        }
        isEncoding = false
     }
Buckley answered 28/11, 2022 at 23:24 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.