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.
ObervableObject
and do the heavy work there. And rather thanDispatchQueue
and completion handlers useasync/await
. – Poky@MainActor
andTask { @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