I have encountered an issue when trying to update the status of a detached task, by passing a closure to MainActor.run
.
To illustrate the issue consider a function that counts the number of files in a folder and its sub-directories. It runs in a Task.detached
closure, as I don't want it blocking the main thread. Every 10,000th file it updates a Published
property fileCount
, by passing a closure to MainThread.run
.
However, the UI is failing to update and even giving me a spinning beach ball. The only way to stop this is by inserting await Task.sleep(1_000_000_000)
before the call to MainThread.run
. Here's the code:
final class NewFileCounter: ObservableObject {
@Published var fileCount = 0
func findImagesInFolder(_ folderURL: URL) {
let fileManager = FileManager.default
Task.detached {
var foundFileCount = 0
let options = FileManager.DirectoryEnumerationOptions(arrayLiteral: [.skipsHiddenFiles, .skipsPackageDescendants])
if let enumerator = fileManager.enumerator(at: folderURL, includingPropertiesForKeys: [], options: options) {
while let _ = enumerator.nextObject() as? URL {
foundFileCount += 1
if foundFileCount % 10_000 == 0 {
let fileCount = foundFileCount
await Task.sleep(1_000_000_000) // <-- Only works with this in...comment out to see failure
await MainActor.run { self.fileCount = fileCount }
}
}
let fileCount = foundFileCount
await MainActor.run { self.fileCount = fileCount }
}
}
}
}
The code works if I revert to the old way of achieving this:
final class OldFileCounter: ObservableObject {
@Published var fileCount = 0
func findImagesInFolder(_ folderURL: URL) {
let fileManager = FileManager.default
DispatchQueue.global(qos: .userInitiated).async {
let options = FileManager.DirectoryEnumerationOptions(arrayLiteral: [.skipsHiddenFiles, .skipsPackageDescendants])
var foundFileCount = 0
if let enumerator = fileManager.enumerator(at: folderURL, includingPropertiesForKeys: [], options: options) {
while let _ = enumerator.nextObject() as? URL {
foundFileCount += 1
if foundFileCount % 10_000 == 0 {
let fileCount = foundFileCount
DispatchQueue.main.async { self.fileCount = fileCount }
}
}
let fileCount = foundFileCount
DispatchQueue.main.async { self.fileCount = fileCount }
}
}
}
}
What am I doing wrong?
BTW - if you want to try out this code, here is a test harness. Be sure to pick a folder with lots of files in it and its sub-folders.
import SwiftUI
@main
struct TestFileCounterApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
@State private var showPickerOld = false
@StateObject private var fileListerOld = OldFileCounter()
@State private var showPickerNew = false
@StateObject private var fileListerNew = NewFileCounter()
var body: some View {
VStack {
Button("Select folder to count files using DispatchQueue...") { showPickerOld = true }
Text("\(fileListerOld.fileCount)").foregroundColor(.green)
.fileImporter(isPresented: $showPickerOld, allowedContentTypes: [.folder], onCompletion: processOldSelectedURL )
Divider()
Button("Select folder to count files using Swift 5.5 concurrency...") { showPickerNew = true }
Text("\(fileListerNew.fileCount)").foregroundColor(.green)
.fileImporter(isPresented: $showPickerNew, allowedContentTypes: [.folder], onCompletion: processNewSelectedURL )
}
.frame(width: 400, height: 130)
}
private func processOldSelectedURL(_ result: Result<URL, Error>) {
switch result {
case .success(let url): fileListerOld.findImagesInFolder(url)
case .failure: return
}
}
private func processNewSelectedURL(_ result: Result<URL, Error>) {
switch result {
case .success(let url): fileListerNew.findImagesInFolder(url)
case .failure: return
}
}
}```