Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to do asynchronous action with SwiftUI button

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.

like image 304
zxcheergo Avatar asked May 22 '26 07:05

zxcheergo


1 Answers

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
     }
like image 153
malhal Avatar answered May 25 '26 10:05

malhal



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!