I’m coming from a PowerShell background, and at the moment trying to get my head around Swift Concurrency. My first project as it relates to concurrency is a utility to en mass transcribe audio files, and I am banging my head on some basics.
What I am trying to do here is simply get a list of the audio files, then loop through them displaying the one being worked on in the UI, and use a simple timer to simulate the transcription work for now. I found [this][1] that I thought would allow me to use a typical asyncAfter timer but it is not working.
My expectation is that the UI shows the current file name, then after a time the console shows the same while the UI shows the next file name, but the UI is showing only the last file name, and all the writes to console suggest that basically every timer is running in parallel.
In a perfect world I would LIKE to transcribe everything in parallel, but that’s another problem, it seems like SFSpeechURLRecognitionRequest doesn’t support this. Indeed, even just in sequence has given me problems, but I am not even 100% sure I AM doing it in sequence, so I am trying to get any head around forcing that, thus the failed code here.
struct ContentView: View {
@State private var fileName: String?
@State private var files: [URL] = []
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("\(fileName ?? "-")")
}
.padding()
.onAppear {
files = getFiles()
processFiles()
}
}
private func getFiles() -> [URL] {
do {
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let path = documentsURL.appendingPathComponent("Voice Memos").absoluteURL
let contents = try FileManager.default.contentsOfDirectory(at: path, includingPropertiesForKeys: nil, options: [])
let files = (contents.filter {$0.pathExtension == "m4a"}).sorted { url1, url2 in
url1.path > url2.path
}
return files
}
catch {
print(error.localizedDescription)
return []
}
}
private func processFiles() {
for file in files {
Task {
await processFile(file)
}
}
}
private func processFile(_ url: URL) async {
fileName = url.lastPathComponent
let seconds = Double.random(in: 10.0...20.0)
Task.synchronous {
DispatchQueue.main.asyncAfter(deadline: .now() + seconds) {
print("\(url.lastPathComponent) \(seconds)")
}
}
}
}
extension Task where Failure == Error {
/// Performs an async task in a sync context.
///
/// - Note: This function blocks the thread until the given operation is finished. The caller is responsible for managing multithreading.
static func synchronous(priority: TaskPriority? = nil, operation: @escaping @Sendable () async throws -> Success) {
let semaphore = DispatchSemaphore(value: 0)
Task(priority: priority) {
defer { semaphore.signal() }
return try await operation()
}
semaphore.wait()
}
}
EDIT: I also tried
let duration: UInt64 = 2_000_000_000 /* 2 seconds */
var seconds: UInt64 { duration/1_000_000_000 }
Task.synchronous { try await Task.sleep(nanoseconds: duration) }
as the timer function, and that just hangs.
As for whoever downvoted instantly, can you at least explain WHY this is such an offensive question?
>Solution :
The files loop your create 4 task run independently, they are not run in a sequence. So the fileName get set to last file’s name immendietly.
for file in files {
Task {
await processFile(file)
}
}
Also you should not mix Semaphore with async await. Here might be a refactor for your code, also I use String for simplicity:
struct ContentView: View {
@State private var fileName: String?
@State private var files: [String] = []
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("\(fileName ?? "-")")
}
.padding()
.onAppear {
files = getFiles()
Task {
await processFiles()
}
}
}
private func getFiles() -> [String] {
["a", "b", "c", "d"]
}
private func processFiles() async {
for file in files {
await processFile(file)
}
}
private func processFile(_ url: String) async {
fileName = url
let seconds = Double.random(in: 10.0...20.0)
await withCheckedContinuation { continuation in
DispatchQueue.main.asyncAfter(deadline: .now() + seconds) {
continuation.resume()
print("\(url) \(seconds)")
}
}
}
}