0

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
        }
    }
}
Philip Pegden
  • 1,732
  • 1
  • 14
  • 33
  • This has been commented on over at Apple's Developer forums. https://developer.apple.com/forums/thread/688352?page=1#685339022 – Philip Pegden Aug 20 '21 at 11:08

1 Answers1

0

This issue has been resolved by Apple - MacOS 12.1 beta 3, Xcode 13.2 Beta 2.

Philip Pegden
  • 1,732
  • 1
  • 14
  • 33