6

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.

George
  • 25,988
  • 10
  • 79
  • 133
zxcheergo
  • 81
  • 1
  • 5
  • Use a **View Model**, an`ObervableObject` and do the heavy work there. And rather than `DispatchQueue` and completion handlers use `async/await`. – vadian Apr 02 '22 at 18:08
  • Thanks for your comments. Could you please provide some more details? How to let the view model do the heavy work without block the UI – zxcheergo Apr 02 '22 at 23:37
  • 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:)` – Nick Rossik Jul 12 '22 at 04:05
  • Does this answer your question? [Trouble running async functions in background threads (concurrency)](https://stackoverflow.com/questions/73538764/trouble-running-async-functions-in-background-threads-concurrency) – lorem ipsum Nov 28 '22 at 23:32

1 Answers1

6

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
     }
malhal
  • 26,330
  • 7
  • 115
  • 133