0

にクロージャーを渡して、切り離されたタスクのステータスを更新しようとしたときに問題が発生しましたMainActor.run

この問題を説明するために、フォルダーとそのサブディレクトリ内のファイル数をカウントする関数を考えてみましょう。Task.detachedメインスレッドをブロックしたくないので、クロージャーで実行されます。10,000 番目のファイルごとに、クロージャを に渡すことによって、Publishedプロパティを更新します。fileCountMainThread.run

ただし、UI の更新に失敗し、回転するビーチ ボールが表示されることさえあります。これを止める唯一の方法は、await Task.sleep(1_000_000_000)への呼び出しの前に挿入することMainThread.runです。コードは次のとおりです。

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 }
            }
        }
    }
}

これを達成する古い方法に戻ると、コードは機能します。

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 }
            }
        }
    }
}

私は何を間違っていますか?

ところで、このコードを試してみたい場合は、ここにテスト ハーネスがあります。たくさんのファイルが含まれるフォルダーとそのサブフォルダーを必ず選択してください。

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
        }
    }
}
4

1 に答える 1